Java中线程工作时会将主内存中的变量读取到线程本地内存
(主内存中变量的副本),因此当某个线程修改主内存中的变量后,是不会被其他线程感知到的。
volatile
关键字可以解决这个问题。当变量被volatile
修饰后,某个线程修改后,其他线程中的变量会立即失效,这样当其他线程需要使用变量时,会去内存中读取新的值。
简单地说:如果一个字段被volatile
修饰,Java线程内存模型会确保所有线程看到这个变量的值是一致的
这里建议先了解下CPU缓存、MESI、LOCK指令 相关知识:计算机组成原理_CPU缓存 、LOCK 指令
1. 术语约定
- 线程工作内存 = 线程本地内存
- 主存 = Main Memory = 多个DRAM内存模块组成 = 内存条
- core = CPU的一个核心 = 处理器
2. 炒冷饭
这里先复习两个知识点:总线嗅探 和 MESI;
总线嗅探(Bus Snooping):本质上是把所有的读写操作通过总线(Bus)广播给所有的CPU核心(core),然后让各个core各自嗅探这些请求,根据自身情况进行响应。
MESI协议:简单描述其已修改(Modified)逻辑,当某个core要修改数据时,它会广播一个独占请求告诉其他所有core我要独占此数据,把你们的数据置为失效状态;core修改完成后,因为持续的嗅探监听总线,如果别的core要读主存这块数据,该缓存行必须回写到主存;这样当其他core要使用该数据时,只能去主存中重新获取(从而保证,某个core修改数据后,其他core获取的都是最新的数据)。
3. volatile
直接查看变量使用volatile
修饰后的代码,将其转换成汇编指令,会发现在写操作时多了一个lock前缀指令;
1 | 此处直接copy书中的示例: |
根据《Intel64 and IA32 Architectures Software Developer’s Manual》手册的描述,lock前缀指令在多核处理器会做以下事情:多个Core的缓存中共同缓存了某内存地址的数据;如果某个Core修改数据(读-修改-写)时使用了LOCK前缀指令,那么LOCK指令结合MESI协议会将其他Core的缓存中对应数据状态置为失效,同时将修改完的数据回写到主内存中,并且保证指令执行的原子性。
LOCK 指令
这里结合MESI协议,大致梳理下流程:(假定,当前core的高速缓存中已经存在该缓存行(变量数据))
如果此时该缓存行(变量数据)状态是
Exclusive(独占)
,那core就直接修改;如果此时该缓存行(变量数据)状态是
Shared(共享)
,那当前core需要发送一条"独占"的请求给总线,总线会通知其他所有core,要求其他core先把它们高速缓存中对应的缓存行(变量数据)标识为Invalid(失效)
状态(如果它们有的话)。然后当前core开始修改缓存行(变量数据),修改完成后,将缓存行(变量数据)状态置为
Modified(已修改)
。core会持续嗅探总线,监听读写请求,对于
Modified(已修改)
状态的缓存行(变量数据),如果别的core要读主存这块数据,该缓存行(变量数据)状态变为Shared(共享)
,必须插入总线、回写到主存。这样当其他core要使用该数据时,只能去主存中重新获取(从而保证,某个core修改数据后,其他core获取的都是最新的数据)。
4. Reference
- 《深入理解计算机系统原理 第三版》