Skip to content

volatile相关

一、volatile两大核心作用

  1. 保证多线程可见性
  2. 禁止指令重排序 不具备原子性,无法保证复合操作(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. 两种解决方案

  1. 启动程序添加VM参数 -Xint,禁用JIT即时编译,不推荐,全局关闭优化损耗性能;
  2. 使用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读写会插入内存屏障,限制指令跨越屏障重排:

  1. volatile写屏障(StoreStore+StoreLoad) 屏障上方所有读写,不能重排到volatile写指令下方;
  2. 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,自动批量压测输出异常场景。

五、面试高频问题解析

  1. 问:volatile有什么作用?

    答: 两点,第一保证共享变量多线程可见性,每次读写强制同步主内存,防止JIT缓存优化;第二通过内存屏障禁止指令重排序。volatile不具备原子性,不能解决i++这类复合操作线程安全问题。

  2. 问:普通变量为什么会出现不可见?

    答: JIT编译器优化循环,将共享变量缓存到线程工作内存,不再重复从主内存加载,其他线程修改主内存后当前线程无法感知,产生死循环。

  3. 问:volatile如何禁止指令重排序?

    答: 读写volatile变量时插入内存屏障,写屏障阻止上方代码重排到volatile写之后;读屏障阻止下方代码重排到volatile读之前,限制指令跨越屏障打乱顺序。

  4. 问:-Xint参数作用与缺点?

    答: -Xint禁用JIT即时编译,仅解释执行,不会做循环缓存优化,临时解决可见性;缺点是关闭全局编译优化,程序运行速度大幅降低,生产环境禁止使用。

  5. 问:volatile和synchronized区别?

    答: volatile仅保证可见性、有序性,无原子性,不阻塞线程;synchronized同时保证可见、有序、原子性,会阻塞竞争线程,性能开销更大。

Powered by VitePress 1.6.4 | 持续更新中