1.Volatile

volatile 是 Java虚拟机提供的一种轻量级的同步机制, 它不会引起线程上下文的切换和调度。 并且使用volitale关键字可以保证可见性、不保证原子性、禁止指令重排序!

保证可见性

  1. //线程1
  2. volitale boolean OK = false;
  3. while(!OK){
  4. doSomething();
  5. }
  6. //线程2
  7. OK = true;

一:使用volatile关键字会强制将修改的值立即写入主存。
二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量ok的缓存行无效。
三:由于线程1的工作内存中缓存变量ok的缓存行无效,所以线程1再次读取变量ok的值时会去主存读取。

不保证原子性

  1. public class TestAtomicity {
  2. public volatile int variable = 0;
  3. //一个自增方法
  4. public void add() {
  5. variable++;
  6. }
  7. public static void main(String[] args) {
  8. final TestAtomicity testAtoc = new TestAtomicity();
  9. for(int i=0;i<20;i++){
  10. new Thread(){
  11. public void run() {
  12. for(int j=0;j<500;j++)
  13. testAtoc.add();
  14. };
  15. }.start();
  16. }
  17. while(Thread.activeCount()>1) //保证前面的线程都执行完
  18. Thread.yield();
  19. System.out.println(testAtoc.variable);//一个小于10000的数字
  20. }
  21. }

++操作的执行过程:

  1. 首先获取变量variable的值
  2. 将该变量的值+1
  3. 将该变量的值写回到对应的主内存中

1号线程刚刚要写1的时候被挂起,2号线程将1写入主内存,此时应该通知其他线程,主内存的值更改为1,由于线程操作极快,还没有通知到其他线程,刚才被挂起的线程1 将num=1 又再次写入了主内存,主内存的值被覆盖,出现了丢失写值
可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
volitale保证原子性的三种方法:

  • 加synchronized关键字
  • 加lock锁
  • 使用原子类(Atomic)借助CAS来实现

    禁止指令重排序

    重排序

    编译器和处理器为了优化程序性能而对指令序列进行重排序,即编写的代码顺序和最终执行的指令顺序是不一致的。但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

    //证明存在重排序
    public class T10_InstructionRearrangement {
      public static int a = 0, b = 0, x = 0, y = 0;
    
      public static void main(String[] args) throws InterruptedException {
          int count = 0;
          while(true) {
              a = 0;
              b = 0;
              x = 0;
              y = 0;
              Thread thread1 = new Thread(() -> {
                  a = 1;
                  x = b;
              });
    
              Thread thread2 = new Thread(() -> {
                  b = 1;
                  y = a;
              });
    
              thread1.start();
              thread2.start();
    
              thread1.join();
              thread2.join();
    
              // 如果没有指令重排序,可能的情况有
              // x = 1 y = 1
              // x = 0 y = 1
              // x = 1 y = 0
              // 当出现了x=0,y=0说明发生了指令重排
              count ++;
              if (x == 0 && y == 0){
                  System.out.println("运行次数:" + count);
                  System.out.printf("x = %d, y = %d\n", x, y);
                  break;
              }
          }
      }
    }
    //结果会出现x = 0,y =0
    

    内存屏障

    内存屏障 是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性。内存屏障会提供3个功能:
    1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    2)它会强制将对缓存的修改操作立即写入主存;
    3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
    image.png

    volatile读写语义

      //x、y为非volatile变量
      //flag为volatile变量
      x = 2;            //语句1
      y = 0;            //语句2
      fag = true;      //语句3
      x = 4;             //语句4
      y = -1;           //语句5
    

    flag变量为volatile变量,在进行指令重排序时,不会将语句3放到语句1、语句2前面,也不会将语句3放到语句4、语句5后面。 语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
    volatile读写:

  1. 在每个volatile写操作的前面插入一个StoreStore屏障;
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障;
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障;
  4. 在每个volatile读操作的后面插入一个LoadStore屏障。

image.png
1642491094291-bca80212-cfc0-45b0-a918-663b9c0b9f7b.png

2.实现原理

字节码层面是ACC_VOLATILE标识。
JVM层面是内存屏障。
操作系统层面是通过lock前缀指令实现的。
lock指令的几个作用:

  1. 锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
  2. lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
  3. 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序

    3.使用场景

    DCL

    public class DoubleCheckLock {
    
     private volatile static DoubleCheckLock instance;
    
     private DoubleCheckLock(){}
    
     public static DoubleCheckLock getInstance(){
    
         //第一次检测
         if (instance==null){
             //同步
             synchronized (DoubleCheckLock.class){
                 if (instance == null){
                     //多线程环境下可能会出现问题的地方,非原子操作
                     instance = new DoubleCheckLock();
                 }
             }
         }
         return instance;
     }
    }
    

    instance = new DoubleCheckLock(), 可分解为: 1.memory =allocate(); //分配对象的内存空间
    2.ctorInstance(memory); //初始化对象
    3.instance =memory; //设置instance指向刚分配的内存地址 有可能出现1,3,2的顺序 ,虽然instance不为空,但是对象也有可能没有正确初始化,会出错。
    Java对象的创建不是原子性操作,所以有指令重排序的可能。为了禁止指令重排序,所以要引入 volatile

volatile和synchronized

  • volitale仅能使用在变量级别,synchronized则可以使用在变量和方法上
  • volitale仅能实现变量的修改可见性(缓存数据不对问题),原子性保证不了;而synchronzied则可以保证变量的修改可见性和原子性
  • volatile不会造成线程阻塞,而Synchronized会造成线程阻塞
  • voltaile本质是在告诉jvm当亲变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问这个变量,其他线程被阻塞