企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] ## 简介 java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。 * 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。 * volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。 当一个变量定义为 volatile 之后,将具备两种特性: ### 可见性 * 当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。 * volatile之所以具有可见性,是因为底层中的Lock指令,该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。 ### 指令重排序优化 * 有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障; * 指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理 * volatile之所以能防止指令重排序,是因为Java编译器对于volatile修饰的变量,会插入内存屏障。内存屏障会防止CPU处理指令的时候重排序的问题 ## 可见性原理 物理计算机为了处理缓存不一致的问题。提出了缓存一致性的协议,其中缓存一致性的核心思想是: > 1\. 当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态 > 2\. 因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。 既然volatile修饰的变量能具有“可见性”,那么volatile内部肯定是走的底层,同时也肯定满足缓存一致性原则。 因为涉及到底层汇编,这里我们不要去了解汇编语言,我们只要知道当用volatile修饰变量时,生成的汇编指令会比普通的变量声明会多一个Lock指令。那么Lock指令会在多核处理器下会做两件事情: 1. 将当前处理器缓存的数据直接写会到系统内存中,从Java内存模型来理解,就是将线程中的工作内存的数据直接写入到主内存中 2. 这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。从Java内存模型理解,当线程A将工作内存的数据修改后(新值),同步到主内存中,那么线程B从主内存中初始的值(旧值)就无效了 从某种意义上,它就相当于:声明变量是 volatile 的,JVM 保证了每次读变量都从主内存中读,跳过 CPU cache这一步。 ## 防止重排序 在《(2.1.27.2)Java并发编程:JAVA的内存模型》我们已经提及“CPU(处理器)会对没有数据依赖性的指令进行重排序”在多线程环境中引发的问题,Java内存模型规定了使用volatile来修饰相应变量时,可以防止CPU(处理器)在处理指令的时候禁止重排序。具体如下图所示 ![](https://img.kancloud.cn/a2/ab/a2ab5bf0d0a19de8b893d577f3144d42_700x148.png) 【volatile防止重排序规则】 从上表我们可以看出 1. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序,这个规则确保voatile写之前的操作不会被编译器排序到volatile之后。 2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。 3. 当第一个操作是volatile写,第二个操作如果是volatile读时,不能进行重排序。 ### 写内存屏障 ![](https://img.kancloud.cn/6e/9f/6e9fc0e94c574bf4f82ec385faaed032_551x517.png) storestore屏障: * 对于这样的语句store1; storestore; store2,在store2及后续写入操作执行前,保证store1的写入操作对其它处理器可见。 * (也就是说如果出现storestore屏障,那么store1指令一定会在store2之前执行,CPU不会store1与store2进行重排序) storeload屏障: * 对于这样的语句store1; storeload; load2,在load2及后续所有读取操作执行前,保证store1的写入对所有处理器可见。 * (也就是说如果出现storeload屏障,那么store1指令一定会在load2之前执行,CPU不会对store1与load2进行重排序) ### 读内存屏障 ![](https://img.kancloud.cn/ef/de/efdeba1c32d0147351ef1dccf0f7888e_551x517.png) loadload屏障: * 对于这样的语句load1; loadload; load2,在load2及后续读取操作要读取的数据被访问前,保证load1要读取的数据被读取完毕。 * (也就是说,如果出现loadload屏障,那么load1指令一定会在load2之前执行,CPU不会对load1与load2进行重排序) loadstore屏障: * 对于这样的语句load1; loadstore; store2,在store2及后续写入操作被刷出前,保证load1要读取的数据被读取完毕。 * (也就是说,如果出现loadstore屏障,那么load1指令一定会在load2之前执行,CPU不会对load1与store2进行重排序) ## volatile的使用条件 现在我们已经了解了volatile的相关特性,那么就来说说,volatile的具体使用场景,因为volatie变量只能保证可见性,并不能保证原子性,这就是说线程能够自动发现 volatile 变量的最新值。所以在轻量级线程同步中我们可以使用volatile关键字。 但是有两个前提条件: * 第一个条件:运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。( 运算不是使 i++ ,而是i = 0) * 第二个条件:变量不需要与其他的状态变量共同参与不变约束。(判断只能有与一个判断) ## volatile的适用场景 ### 模式1:状态标志 也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。 ~~~ volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } ~~~ ### 模式2:一次性安全发布(one-time safe publication) 在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。这就是造成著名的双重检查锁定(double-check在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象。 ~~~ //注意volatile!!!!!!!!!!!!!!!!! private volatile static Singleton instace; public static Singleton getInstance(){ //第一次null检查 if(instance == null){ synchronized(Singleton.class) { //1 //第二次null检查 if(instance == null){ //2 instance = new Singleton();//3 } } } return instance; } ~~~ 如果不用volatile,则因为内存模型允许所谓的“无序写入”,可能导致失败。——某个线程可能会获得一个未完全初始化的实例。 ### 模式3:开销较低的“读-写锁”策略 如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。 如下显示的线程安全的计数器,使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。 ~~~ ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; //读操作,没有synchronized,提高性能 public int getValue() { return value; } //写操作,必须synchronized。因为x++不是原子操作 public synchronized int increment() { return value++; } } ~~~ ## 参考资料 [Java并发编程:Volatile](https://blog.csdn.net/fei20121106/article/details/83268153)