Java中的 volatile 关键字

说这个之前,要先说到cpu的运行,大家都知道,计算机在执行程序时,每条指令都是在 CPU 中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟 CPU 执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在 CPU 里面就有了高速缓存。如果程序中存在有被多个线程访问的变量也就是共享变量,有可能就会造成缓存一致性问题(有个缓存一致性协议,不过,他是硬件层面的,叫MESI 协议)。
在多核 CPU 中(现在应该都是多核了),每条线程可能运行于不同的 CPU 中,因此每个线程运行时有自己的高速缓存,所以高速缓存中的变量没有立即写入主存,就会造成可见性问题(也就是其他线程不可见)。
为了解决可见性问题:
第一张方法:就是这里的volatile 关键字,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
第二种:通过 synchronized 和 Lock 也能够保证可见性,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
为了解决缓存不一致性问题:
使用缓存一致性协议(硬件层面)。最出名的就是 Intel 的 MESI 协议,MESI 协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他 CPU 中也存在该变量的副本,会发出信号通知其他 CPU 将该变量的缓存行置为无效状态(反映到硬件层的话,就是CPU 的 L1 或者 L2 缓存中对应的缓存行无效),因此当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

volatile还跟变量的原子性有关,有个经典的自增问题(i++),即使用锁或者volatile都不能保证原子性。要想解决就得用原子类,在 java 1.5的 java.util.concurrent.atomic 包下提供了一些原子操作类,即对基本数据类型的 自增(加 1操作),自减(减 1 操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic 是利用 CAS 来实现原子性操作的(Compare And Swap),CAS 实际上是利用处理器提供的 CMPXCHG 指令实现的,而处理器执行 CMPXCHG 指令是一个原子性操作。
还有一点, volatile 关键字能禁止指令重排序,所以 volatile 能在一定程度上保证有序性。
举个例子:

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

由于 flag 变量为 volatile 变量,那么在进行指令重排序的过程的时候,不会将语句 3 放到语句 1、语句 2 前面,也不会讲语句 3 放到语句 4、语句 5 后面。但是要注意语句 1 和语句 2 的顺序、语句 4 和语句 5 的顺序是不作任何保证的。
并且 volatile 关键字能保证,执行到语句 3 时,语句 1 和语句 2 必定是执行完毕了的,且语句 1 和语句 2 的执行结果对语句 3、语句 4、语句 5 是可见的。
说一下原理:volatile 的原理和实现机制:
观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令
lock 前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供 3 个功能:
1.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2.它会强制将对缓存的修改操作立即写入主存;
3.如果是写操作,它会导致其他 CPU 中对应的缓存行无效。
volatile的金典使用场景:双重检查

class Singleton{
    private volatile static Singleton instance = null;
     
    private Singleton() {
         
    }
     
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

据说这样的双重检查有问题,可以使用ThreadLocal修复双重检测

public class Singleton {  
 private static final ThreadLocal perThreadInstance = new ThreadLocal();  
 private static Singleton singleton ;  
 private Singleton() {}  
   
 public static Singleton  getInstance() {  
  if (perThreadInstance.get() == null){  
   // 每个线程第一次都会调用  
   createInstance();  
  }  
  return singleton;  
 }  
  
 private static  final void createInstance() {  
  synchronized (Singleton.class) {  
   if (singleton == null){  
    singleton = new Singleton();  
   }  
  }  
  perThreadInstance.set(perThreadInstance);  
 }  
}

有个原子性操作案例:

x = 10;         //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

有些朋友可能会说上面的 4 个语句中的操作都是原子性操作。其实只有语句 1 是原子性操作,其他三个语句都不是原子性操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章