volatile实现机制详解
1. volatile的两大特点
1.1 可见性
写完后立即刷新回主内存并及时发出通知,大家可以去主内存拿最新版,前面的修改对后面的线程可见。
1.2 有序性
有时禁止指令重排。重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序:
- 不存在数据依赖关系,可以重排序
- 存在数据依赖关系,禁止重排序
- 重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
1.3 不支持原子性
volatile无法保证原子性,这是其重要限制。
1.4 volatile的内存语义
- 写语义 :当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中
- 读语义:当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
volatile凭什么可以保证可见性和有序性?答案是:内存屏障Memory Barrier
2. 内存屏障机制
2.1 什么是内存屏障
内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。
2.2 内存屏障的作用
- 内存屏障之前的所有写操作都要回写到主内存
- 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
2.3 内存屏障分类
粗分2种
- 读屏障(Load Barrier):在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据
- 写屏障(Store Barrier):在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中
细分4种
- LoadLoad屏障:确保Load1数据的装载先于Load2及所有后续装载指令的装载
- StoreStore屏障:确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储
- LoadStore屏障:确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存
- StoreLoad屏障:确保Store1数据对其他处理器变得可见(刷新到内存)先于Load2及所有后续装载指令的装载
重新从主内存获取] E --> K[强制刷新写缓冲区
到主内存] F --> L[Load1 先于 Load2] G --> M[Store1 先于 Store2] H --> N[Load1 先于 Store2] I --> O[Store1 先于 Load2]
3. volatile实现机制详解
3.1 volatile变量的内存屏障插入策略
JVM针对volatile变量会在特定位置插入内存屏障:
-
volatile写操作:
- 在volatile写前插入StoreStore屏障
- 在volatile写后插入StoreLoad屏障
-
volatile读操作:
- 在volatile读后插入LoadLoad屏障
- 在volatile读后插入LoadStore屏障
3.2 实例分析:四种内存屏障的应用
我们通过一个完整的volatile示例来详细分析四种内存屏障的应用机制。
示例场景
java
public class VolatileBarrierDemo {
private int normalVar1 = 0; // 普通变量1
private int normalVar2 = 0; // 普通变量2
private volatile int flag = 0; // volatile变量
private int normalVar3 = 0; // 普通变量3
// 线程A:写操作
public void writer() {
normalVar1 = 1; // 普通写1
normalVar2 = 2; // 普通写2
flag = 1; // volatile写 (关键屏障点)
normalVar3 = 3; // 普通写3
}
// 线程B:读操作
public void reader() {
if (flag == 1) { // volatile读 (关键屏障点)
int temp1 = normalVar1; // 普通读1
int temp2 = normalVar2; // 普通读2
int temp3 = normalVar3; // 普通读3
}
}
}
字节码分析
使用javap -c -v VolatileBarrierDemo
查看字节码:
java
// writer方法字节码
public void writer();
Code:
0: aload_0
1: iconst_1
2: putfield #2 // Field normalVar1:I
5: aload_0
6: iconst_2
7: putfield #3 // Field normalVar2:I
10: aload_0
11: iconst_1
12: putfield #4 // Field flag:I (volatile字段)
15: aload_0
16: iconst_3
17: putfield #5 // Field normalVar3:I
20: return
// reader方法字节码
public void reader();
Code:
0: aload_0
1: getfield #4 // Field flag:I (volatile字段)
4: iconst_1
5: if_icmpne 25
8: aload_0
9: getfield #2 // Field normalVar1:I
12: istore_1
13: aload_0
14: getfield #3 // Field normalVar2:I
17: istore_2
18: aload_0
19: getfield #5 // Field normalVar3:I
22: istore_3
23: goto 25
26: return
关键观察点:
putfield #4
和getfield #4
是对volatile字段的操作- JVM会在这些指令周围插入内存屏障
重要说明 :JVM的内存屏障插入不是在字节码阶段 ,而是在JIT编译时 和解释执行时分别处理的。
内存屏障插入策略详解
详细屏障机制分析
1. volatile写操作中的内存屏障
StoreStore屏障(在volatile写之前):
java
// JVM插入的内存屏障(概念性表示)
normalVar1 = 1; // 普通写1
normalVar2 = 2; // 普通写2
// ========== StoreStore屏障 ==========
flag = 1; // volatile写
物理实现机制:
- x86指令:隐式实现,通过写缓冲区刷新
- ARM指令 :
DMB ST
(数据内存屏障-存储) - 作用:确保所有在volatile写之前的普通写操作都刷新到主内存
缓存一致性协议(MESI)作用:
- Modified状态:普通写操作使缓存行进入Modified状态
- Invalid状态:StoreStore屏障触发后,其他CPU的对应缓存行变为Invalid
- Shared状态:数据刷新到主内存后,其他CPU可以读取到共享状态
StoreLoad屏障(在volatile写之后):
java
// JVM插入的内存屏障(概念性表示)
flag = 1; // volatile写
// ========== StoreLoad屏障 ==========
normalVar3 = 3; // 普通写3
物理实现机制:
- x86指令 :
MFENCE
(完整内存栅栏) - ARM指令 :
DMB
(数据内存屏障) - 作用:确保volatile写操作对所有CPU全局可见,防止后续操作重排序
2. volatile读操作中的内存屏障
LoadLoad屏障(在volatile读之后):
java
// JVM插入的内存屏障(概念性表示)
if (flag == 1) { // volatile读
// ========== LoadLoad屏障 ==========
int temp1 = normalVar1; // 普通读1
int temp2 = normalVar2; // 普通读2
int temp3 = normalVar3; // 普通读3
}
物理实现机制:
- x86指令 :
LFENCE
(加载栅栏) - ARM指令 :
DMB LD
(数据内存屏障-加载) - 作用:确保volatile读完成后,后续的读操作才能执行
LoadStore屏障(在volatile读之后):
java
// JVM插入的内存屏障(概念性表示)
if (flag == 1) { // volatile读
// ========== LoadStore屏障 ==========
// 防止后续写操作重排序到volatile读之前
// 如果这里有写操作,不能重排序到volatile读之前
}
物理实现机制:
- x86指令:通常与LoadLoad屏障合并实现
- ARM指令 :
DMB
(数据内存屏障) - 作用:防止volatile读之后的写操作重排序到volatile读之前
四种屏障的协作机制
缓存一致性协议详解
MESI协议状态转换
具体协议交互过程
写操作时的协议交互:
-
StoreStore屏障触发:
- CPU1缓存行状态:Modified
- 发送
Invalidate
消息到总线 - 其他CPU将对应缓存行标记为Invalid
-
StoreLoad屏障触发:
- CPU1等待所有CPU确认
Invalidate
- 强制将数据写回主内存
- 广播数据已更新的信号
- CPU1等待所有CPU确认
读操作时的协议交互:
-
LoadLoad屏障触发:
- 检查本地缓存行状态
- 如果状态为Invalid,从主内存重新加载
- 缓存行状态变为Shared
-
LoadStore屏障触发:
- 确保后续写操作不会重排序
- 维护内存访问的顺序性
可见性保证的完整链路
性能开销分析
屏障类型 | x86开销 | ARM开销 | 主要影响 |
---|---|---|---|
StoreStore | 低 | 中 | 写缓冲区刷新 |
StoreLoad | 高 | 高 | 完整内存栅栏 |
LoadLoad | 低 | 中 | 预读缓冲区清空 |
LoadStore | 低 | 中 | 防止重排序 |
关键洞察:
- StoreLoad屏障开销最高,因为需要等待所有CPU确认
- LoadLoad屏障确保读取最新数据,避免读取脏数据
- 四种屏障协作确保volatile语义的正确实现
实际应用场景
通过这个例子,我们可以看到:
- 写端保证 :当线程A执行完
flag=1
时,normalVar1=1
和normalVar2=2
必然已经对所有线程可见 - 读端保证 :当线程B读取到
flag==1
时,必然能够读取到normalVar1=1
和normalVar2=2
- 有序性保证 :
normalVar3=3
的写入不会重排序到flag=1
之前 - 可见性保证:通过缓存一致性协议确保数据的全局可见性
这就是volatile通过内存屏障实现可见性和有序性的完整机制。
4. 四种内存屏障的深度解析
4.1 内存屏障的精确定义与缓存一致性协议实现
LoadLoad屏障详解
定义:确保Load1数据的装载先于Load2及所有后续装载指令的装载
缓存一致性协议实现机制:
物理实现细节:
- x86架构 :
LFENCE
指令,阻止后续读操作越过屏障 - ARM架构 :
DMB LD
指令,确保读操作顺序 - RISC-V架构 :
fence r,r
指令,读-读屏障
缓存行状态转换:
- Invalid → Shared:从主内存加载数据,其他CPU可共享
- 维持Shared状态:多个CPU可以同时读取相同数据
- 预读缓冲区清空:防止乱序执行单元提前读取后续数据
StoreStore屏障详解
定义:确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储
缓存一致性协议实现机制:
物理实现细节:
- x86架构:隐式实现,写操作天然有序
- ARM架构 :
DMB ST
指令,存储-存储屏障 - RISC-V架构 :
fence w,w
指令,写-写屏障
MESI状态转换关键点:
- Modified → Invalid:其他CPU的缓存行失效
- 写回主内存:确保Store1全局可见
- 写缓冲区刷新:强制提交所有待写数据
LoadStore屏障详解
定义:确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存
缓存一致性协议实现机制:
物理实现细节:
- x86架构:通常与LoadLoad屏障合并实现
- ARM架构 :
DMB
全屏障指令 - RISC-V架构 :
fence r,w
指令,读-写屏障
防止重排序机制:
- 乱序执行阻断:防止存储单元提前执行Store2
- 内存依赖检查:确保Load1的数据依赖得到满足
- 写缓冲区排序:维护写操作的程序顺序
StoreLoad屏障详解
定义:确保Store1数据对其他处理器变得可见(刷新到内存)先于Load2及所有后续装载指令的装载
缓存一致性协议实现机制(最复杂的屏障):
物理实现细节:
- x86架构 :
MFENCE
指令,完整内存栅栏 - ARM架构 :
DMB
全屏障指令 - RISC-V架构 :
fence rw,rw
指令,完整屏障
关键性能开销:
- 流水线暂停:等待所有CPU确认缓存失效
- 写缓冲区刷新:强制提交所有待写数据
- 总线仲裁:等待系统总线完成所有传输
JVM内存屏障插入时机详解
重要说明 :JVM的内存屏障插入不是在字节码阶段 ,而是在JIT编译时 和解释执行时分别处理的。
1. 字节码阶段的volatile标识
在字节码层面,volatile字段通过**访问标志(Access Flags)**来标识,而不是通过额外的屏障指令:
java
// 使用javap -v -p查看字段的访问标志
private volatile int flag;
// 对应的字节码常量池和字段描述
Constant pool:
#4 = Fieldref #1.#19 // VolatileBarrierDemo.flag:I
#19 = NameAndType #20:#21 // flag:I
// 字段访问标志
private volatile int flag;
descriptor: I
flags: ACC_PRIVATE, ACC_VOLATILE // 关键:ACC_VOLATILE标志
字节码指令本身没有变化:
java
// volatile写操作的字节码
10: aload_0
11: iconst_1
12: putfield #4 // Field flag:I
// 注意:这里的putfield指令本身没有特殊性
// volatile语义由字段的ACC_VOLATILE标志决定
// volatile读操作的字节码
0: aload_0
1: getfield #4 // Field flag:I
// 同样,getfield指令本身没有特殊性
2. JIT编译时的内存屏障插入
真正的内存屏障插入发生在JIT编译器将字节码编译成机器码时:
java
// JIT编译器的伪代码处理逻辑
public void compileVolatileWrite(FieldInstruction putfield) {
Field field = putfield.getField();
if (field.hasFlag(ACC_VOLATILE)) {
// 1. 插入StoreStore屏障
emitStoreStoreBarrier();
// 2. 编译实际的写操作
emitStoreInstruction(putfield);
// 3. 插入StoreLoad屏障
emitStoreLoadBarrier();
} else {
// 普通字段写操作
emitStoreInstruction(putfield);
}
}
public void compileVolatileRead(FieldInstruction getfield) {
Field field = getfield.getField();
if (field.hasFlag(ACC_VOLATILE)) {
// 1. 编译实际的读操作
emitLoadInstruction(getfield);
// 2. 插入LoadLoad屏障
emitLoadLoadBarrier();
// 3. 插入LoadStore屏障
emitLoadStoreBarrier();
} else {
// 普通字段读操作
emitLoadInstruction(getfield);
}
}
3. 解释执行时的内存屏障处理
在解释器模式下,内存屏障效果通过特殊的字节码处理逻辑实现:
java
// HotSpot解释器的伪代码
void BytecodeInterpreter::doPutField() {
// 获取字段描述符
ConstantPoolCacheEntry* cache = cpool->entry_at(index);
Field* field = cache->field();
if (field->is_volatile()) {
// volatile写操作的特殊处理
// 1. 执行StoreStore屏障效果
OrderAccess::storestore(); // 平台相关的内存屏障实现
// 2. 执行实际的写操作
field->set_value(object, value);
// 3. 执行StoreLoad屏障效果
OrderAccess::storeload(); // 平台相关的内存屏障实现
} else {
// 普通字段写操作
field->set_value(object, value);
}
}
void BytecodeInterpreter::doGetField() {
ConstantPoolCacheEntry* cache = cpool->entry_at(index);
Field* field = cache->field();
if (field->is_volatile()) {
// volatile读操作的特殊处理
// 1. 执行实际的读操作
value = field->get_value(object);
// 2. 执行LoadLoad屏障效果
OrderAccess::loadload(); // 平台相关的内存屏障实现
// 3. 执行LoadStore屏障效果
OrderAccess::loadstore(); // 平台相关的内存屏障实现
} else {
// 普通字段读操作
value = field->get_value(object);
}
}
4. 完整的编译和执行流程
volatile int flag] --> B[javac编译] B --> C[字节码
putfield #4
ACC_VOLATILE标志] C --> D{JVM执行模式} D -->|解释执行| E[解释器] D -->|编译执行| F[JIT编译器] E --> G[检查ACC_VOLATILE标志] G --> H[调用OrderAccess::storestore
执行写操作
调用OrderAccess::storeload] F --> I[检查ACC_VOLATILE标志] I --> J[生成机器码
插入MFENCE等指令] H --> K[解释器内存屏障效果] J --> L[硬件内存屏障指令] style C fill:#e1f5fe style G fill:#fff3e0 style I fill:#fff3e0 style K fill:#f3e5f5 style L fill:#f3e5f5
5. 实际的机器码生成示例
JIT编译后的机器码(x86-64平台):
assembly
// volatile写操作编译后的机器码
// normalVar1 = 1; normalVar2 = 2;
mov $0x1, 0x10(%r8) // normalVar1 = 1
mov $0x2, 0x14(%r8) // normalVar2 = 2
// StoreStore屏障(x86隐式保证)
// 在x86架构上,写操作天然有序,无需显式指令
// flag = 1; (volatile写)
mov $0x1, 0x18(%r8) // flag = 1
// StoreLoad屏障
mfence // 完整内存栅栏指令
// normalVar3 = 3;
mov $0x3, 0x1c(%r8) // normalVar3 = 3
assembly
// volatile读操作编译后的机器码
// if (flag == 1)
mov 0x18(%r8), %eax // 读取flag值(volatile读)
// LoadLoad屏障
lfence // 读屏障指令
// LoadStore屏障(x86通常与LoadLoad合并)
// 在x86架构上,读操作不会重排序到后续写操作之前
cmp $0x1, %eax // 比较flag值
jne end_block // 如果不等于1,跳出
// temp1 = normalVar1; temp2 = normalVar2; temp3 = normalVar3;
mov 0x10(%r8), %ebx // 读取normalVar1
mov 0x14(%r8), %ecx // 读取normalVar2
mov 0x1c(%r8), %edx // 读取normalVar3
6. 不同JVM实现的差异
HotSpot JVM:
java
// OrderAccess类的平台特定实现
class OrderAccess {
// x86平台实现
static void storestore() { /* 隐式保证,无需指令 */ }
static void storeload() { asm volatile("mfence"); }
static void loadload() { asm volatile("lfence"); }
static void loadstore() { /* 隐式保证,无需指令 */ }
// ARM平台实现
static void storestore() { asm volatile("dmb st"); }
static void storeload() { asm volatile("dmb"); }
static void loadload() { asm volatile("dmb ld"); }
static void loadstore() { asm volatile("dmb"); }
}
OpenJ9 JVM:
java
// 类似的实现,但可能有不同的优化策略
class MemoryBarrier {
static void writeBarrier() { /* 平台特定实现 */ }
static void readBarrier() { /* 平台特定实现 */ }
static void fullBarrier() { /* 平台特定实现 */ }
}
7. 验证内存屏障插入的方法
使用-XX:+PrintAssembly查看实际生成的机器码:
bash
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogVMOutput VolatileBarrierDemo
输出示例:
assembly
# volatile写操作
0x00007f8b2c001234: mov $0x1,0x10(%r8) # 普通写
0x00007f8b2c001238: mov $0x1,0x18(%r8) # volatile写
0x00007f8b2c00123c: mfence # StoreLoad屏障!
0x00007f8b2c00123f: mov $0x3,0x1c(%r8) # 后续写
# volatile读操作
0x00007f8b2c001250: mov 0x18(%r8),%eax # volatile读
0x00007f8b2c001254: lfence # LoadLoad屏障!
0x00007f8b2c001257: mov 0x10(%r8),%ebx # 后续读
8. 关键总结
- 字节码层面 :volatile通过
ACC_VOLATILE
标志标识,指令本身不变 - JIT编译时:根据标志插入实际的内存屏障机器指令
- 解释执行时:通过特殊的处理逻辑实现内存屏障效果
- 平台差异:不同CPU架构有不同的屏障指令实现
- 性能影响:编译执行的屏障效果更高效,解释执行开销更大
这就是JVM实现volatile语义的完整技术路径,从字节码的标识到最终硬件指令的生成。
4.3 volatile在不同架构下的实现差异
x86架构的volatile实现
特点:
- 强内存模型:x86天然保证写操作顺序
- 隐式屏障:某些屏障由硬件自动实现
- MFENCE开销:StoreLoad屏障需要显式指令
ARM架构的volatile实现
特点:
- 弱内存模型:需要显式屏障指令
- 细粒度控制:不同类型的DMB指令
- 性能优化:可以针对特定操作类型优化
4.4 内存屏障的性能影响分析
不同屏障的性能开销对比
开销最高
100-300周期] B[StoreStore屏障
中等开销
10-50周期] C[LoadLoad屏障
较低开销
5-20周期] D[LoadStore屏障
较低开销
5-20周期] A --> B B --> C C --> D end
实际应用中的优化策略
- 减少volatile使用:仅在必要时使用volatile
- 批量操作:合并多个volatile操作
- 读写分离:优化读多写少的场景
- 架构感知:针对特定架构优化
4.5 总结:volatile的完整实现链路
通过以上分析,我们可以看到volatile的完整实现链路:
- Java源码层面:声明volatile变量
- 编译器层面:插入内存屏障指令
- JVM层面:将屏障指令转换为机器码
- 硬件层面:执行具体的内存屏障操作
- 缓存协议层面:维护多核间的缓存一致性
每个层面都有其特定的职责和实现机制,共同保证了volatile变量的可见性和有序性语义。