volatile
2025-01-22 08:19:30    1k 字   
This post is also available in English and alternative languages.

Java中线程工作时会将主内存中的变量读取到线程本地内存(主内存中变量的副本),因此当某个线程修改主内存中的变量后,是不会被其他线程感知到的。

volatile关键字可以解决这个问题。当变量被volatile修饰后,某个线程修改后,其他线程中的变量会立即失效,这样当其他线程需要使用变量时,会去内存中读取新的值。
简单地说:如果一个字段被volatile修饰,Java线程内存模型会确保所有线程看到这个变量的值是一致的

这里建议先了解下CPU缓存、MESI、LOCK指令 相关知识:计算机组成原理_CPU缓存LOCK 指令


1. 术语约定

  1. 线程工作内存 = 线程本地内存
  2. 主存 = Main Memory = 多个DRAM内存模块组成 = 内存条
  3. core = CPU的一个核心 = 处理器

2. 炒冷饭

这里先复习两个知识点:总线嗅探MESI

总线嗅探(Bus Snooping):本质上是把所有的读写操作通过总线(Bus)广播给所有的CPU核心(core),然后让各个core各自嗅探这些请求,根据自身情况进行响应。

MESI协议:简单描述其已修改(Modified)逻辑,当某个core要修改数据时,它会广播一个独占请求告诉其他所有core我要独占此数据,把你们的数据置为失效状态;core修改完成后,因为持续的嗅探监听总线,如果别的core要读主存这块数据,该缓存行必须回写到主存;这样当其他core要使用该数据时,只能去主存中重新获取(从而保证,某个core修改数据后,其他core获取的都是最新的数据)。

知识点详情:计算机组成原理_CPU缓存#缓存一致性(Cache Coherency)


3. volatile

直接查看变量使用volatile修饰后的代码,将其转换成汇编指令,会发现在写操作时多了一个lock前缀指令;

1
2
此处直接copy书中的示例:
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

根据《Intel64 and IA32 Architectures Software Developer’s Manual》手册的描述,lock前缀指令在多核处理器会做以下事情:多个Core的缓存中共同缓存了某内存地址的数据;如果某个Core修改数据(读-修改-写)时使用了LOCK前缀指令,那么LOCK指令结合MESI协议会将其他Core的缓存中对应数据状态置为失效,同时将修改完的数据回写到主内存中,并且保证指令执行的原子性。

LOCK 指令

这里结合MESI协议,大致梳理下流程:(假定,当前core的高速缓存中已经存在该缓存行(变量数据))

  1. 如果此时该缓存行(变量数据)状态是Exclusive(独占),那core就直接修改;

  2. 如果此时该缓存行(变量数据)状态是Shared(共享),那当前core需要发送一条"独占"的请求给总线,总线会通知其他所有core,要求其他core先把它们高速缓存中对应的缓存行(变量数据)标识为Invalid(失效)状态(如果它们有的话)。

  3. 然后当前core开始修改缓存行(变量数据),修改完成后,将缓存行(变量数据)状态置为Modified(已修改)

  4. core会持续嗅探总线,监听读写请求,对于Modified(已修改)状态的缓存行(变量数据),如果别的core要读主存这块数据,该缓存行(变量数据)状态变为Shared(共享),必须插入总线、回写到主存。

    这样当其他core要使用该数据时,只能去主存中重新获取(从而保证,某个core修改数据后,其他core获取的都是最新的数据)。


4. Reference

  • 《深入理解计算机系统原理 第三版》