volatile关键字详解

volatile关键字有两个作用,一是保证变量对所有线程可见,即一个线程修改了变量,其他线程马上就能得到新的值;二是禁止指令重排,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

从Java内存模型看volatile

不同架构的物理机拥有不一样的内存模型。Java的宗旨就是:一次编译,到处运行。为了屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各个平台下都能达到一致的内存访问效果,Java虚拟机规范定义了(抽象出)Java内存模型(Java Memory Model,JMM)。JMM规定了所有的变量都存储在主内存中,每条线程都有自己的工作内存,线程的工作内存中保存该线程使用的变量的主内存副本,线程对变量的所有操作必须在工作内存中进行,而不能直接读写内存中的数据。线程、主内存、工作内存之间的关旭如下图所示:

一个变量如何从主内存拷贝到工作内存,又如何从工作内存同步回主内存,JMM定义了一下8种操作来完成,虚拟机实现时必须保证每一个操作的原子性。

  1. lock(锁定):作用于主内存的变量,将变量标识为一条线程独占的状态。
  2. unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

Java内存模型对volatile变量定义了一些特殊的规则,假定T1和T2表示两个线程,变量x和y表示两个volatile变量:

  • 规则一:只有当T1对x执行的前一个动作是load时,T1才能对x执行use动作;并且只有当T1对x执行的后一个动作是use时,T1才能对x执行load动作。这条规则要求每次使用变量前必须从主存中读取,用于保证能够看见其他线程对该变量的修改。
  • 规则二:只有当T1对x执行的前一个动作是assign时,T1才能对x执行store动作;并且只有当T1对x执行的后一个动作是store时,T1才能对x执行assign动作。这条规则要求每次修改变量后必须立刻同步回主内存中,用于保证其他线程能够看见对该变量的修改。
  • 规则三:假设动作A是T1对x的use或assign动作,P和F是与A关联的read和write动作;动作B是T1对y的use或assign动作,Q和G是与B关联的read和write动作。如果A先与B,那么P先与Q。这条规则要求volatile修饰的变量不会被指令重排,代码的执行顺序与程序的顺序相同。

从处理器内存模型看volatile

为了解决CPU运算速度与内存读写速度不匹配的矛盾,CPU厂商设计了CPU高速缓存,程序在运行过程中,会从主存复制一份数据到CPU的高速缓存中,CPU进行计算时就可以直接从它的高速缓存读取数据以及向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。在单核CPU中这不会存在什么问题,但是在多核的情况下就会出现缓存不一致。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就需要缓存一致性协议,它的基本思想是:每个处理器都在不停在嗅探总线上发生的数据交换,跟踪其他处理器在做什么,所以当一个处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步,只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存中已失效。在硬件层面,处理器、高速缓存、主内存之间的关系如下图所示:

那么,volatile是如何保证可见性的呢?可以在JIT编译器生成的汇编指令中发现,有volatile修饰的变量在进行写操作时会多执行一条有lock前缀的指令,lock前缀指令有以下几个作用:

  • 将当前处理器里该缓存行的数据回写到内存,这个操作会使其他CPU里该缓存行失效。
  • 相当于一个内存屏障,禁止屏障两边的指令重排。

从lock指令看volatile变量的读写规则:如果一个处理器要对volatile变量进行写操作,处理器会发出LOCK#指令锁住该变量对应的缓存行,同时其他处理器内部对应的缓存行失效(如果有两个及以上处理器发出该指令,总线会仲裁),处理器回写主存后释放锁,其他处理器通过嗅探技术知道了该缓存行已经失效,在下次访问这个内存地址时,强制执行缓存行填充。volatile变量的读和普通变量的读相比基本没差别。
注意:这个点一开始困扰了我很久,后来才知道,JMM中线程的工作内存,是CPU的寄存器和高速缓存的抽象描述,从硬件角度看,JMM的主内存就是硬件内存,为了获取更高的运行速度,虚拟机及硬件系统会将工作内存优先存储与寄存器和高速缓存中。

总结

volatile关键字不能保证原子性,所以它只适用于对变量的写操作不依赖当前值,或者只有一个线程对volatile变量进行读写,而其他的线程只是读取这个变量的情况,比如单例模式,因为对变量的写操作不一定是原子操作,比如自增,需要先读取,然后加一,最后写入内存,如果多个线程同时读取volatile变量的值,然后由此计算新的值,再写回内存就会互相覆盖,这就需要加锁来保证原子性。

参考:
https://zhuanlan.zhihu.com/p/145902867
https://www.cnblogs.com/xrq730/p/7048693.html

posted @   学海无涯#  阅读(558)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
点击右上角即可分享
微信分享提示