Java中的 volatile 关键字

    技术2023-11-12  107

    说这个之前,要先说到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 是原子性操作,其他三个语句都不是原子性操作。

    Processed: 0.014, SQL: 9