主题切换
volatile相关
一、volatile两大核心作用
- 保证多线程可见性
- 禁止指令重排序 不具备原子性,无法保证复合操作(i++)线程安全。
二、保证可见性
1. 问题根源
JIT即时编译器会做循环优化:线程循环读取普通共享变量时,只会读取一次工作内存副本,不再从主内存load,导致其他线程修改后主内存新值无法感知,无限循环。 示例代码:
java
static boolean stop = false;
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
}).start();
// 无volatile会无限循环
new Thread(() -> {
int i = 0;
while (!stop) {
i++;
}
}).start();2. 两种解决方案
- 启动程序添加VM参数
-Xint,禁用JIT即时编译,不推荐,全局关闭优化损耗性能; - 使用
volatile修饰共享变量,禁止JIT缓存优化,每次读写强制同步主内存。
3. 可见性底层逻辑
- 写volatile:写完立即将工作内存新值
save刷新到主内存; - 读volatile:读取前强制从主内存
load最新值到工作内存; - 线程之间修改实时同步,互相可见。
三、禁止指令重排序
1. 指令重排序问题
CPU、编译器为提升执行效率,会打乱代码执行顺序,导致多线程出现异常结果。 示例:
java
int x,y;
void actor1(){
x = 1;
y = 1;
}
void actor2(Result r){
r.r1 = y;
r.r2 = x;
}无volatile时可能出现r1=1,r2=0,说明y=1重排到x=1之前。
2. 内存屏障机制
volatile读写会插入内存屏障,限制指令跨越屏障重排:
- volatile写屏障(StoreStore+StoreLoad) 屏障上方所有读写,不能重排到volatile写指令下方;
- volatile读屏障(LoadLoad+LoadStore) 屏障下方所有读写,不能重排到volatile读指令上方。
3. volatile最佳书写规范
- 写操作:把volatile变量写放在方法代码末尾;
- 读操作:把volatile变量读放在方法代码最开头; 最大化屏障约束范围,避免重排序漏洞。
四、相关补充问题
1. -Xint VM参数添加位置
- IDEA/Eclipse:运行配置 → VM options,填入
-Xint; - 命令行启动:
java -Xint 主类名; 作用:关闭JIT即时编译,纯解释执行,解决可见性但性能大幅下降。
2. jcstress测试工具
Java并发压力测试框架,专门验证volatile、锁、CAS等并发语义是否存在重排序、可见性bug,自动批量压测输出异常场景。
五、面试高频问题解析
问:volatile有什么作用?
答: 两点,第一保证共享变量多线程可见性,每次读写强制同步主内存,防止JIT缓存优化;第二通过内存屏障禁止指令重排序。volatile不具备原子性,不能解决i++这类复合操作线程安全问题。
问:普通变量为什么会出现不可见?
答: JIT编译器优化循环,将共享变量缓存到线程工作内存,不再重复从主内存加载,其他线程修改主内存后当前线程无法感知,产生死循环。
问:volatile如何禁止指令重排序?
答: 读写volatile变量时插入内存屏障,写屏障阻止上方代码重排到volatile写之后;读屏障阻止下方代码重排到volatile读之前,限制指令跨越屏障打乱顺序。
问:-Xint参数作用与缺点?
答: -Xint禁用JIT即时编译,仅解释执行,不会做循环缓存优化,临时解决可见性;缺点是关闭全局编译优化,程序运行速度大幅降低,生产环境禁止使用。
问:volatile和synchronized区别?
答: volatile仅保证可见性、有序性,无原子性,不阻塞线程;synchronized同时保证可见、有序、原子性,会阻塞竞争线程,性能开销更大。