Volatile关键字
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
保证线程间的可见性
禁止进行指令重排序
保证线程间的可见性
public class ForeverLoop {
// 静态变量,类加载时初始化为false(所有线程共享)
// 问题:未声明volatile,导致多线程可见性问题
static boolean flag = false;
public static void main(String[] args) {
// 创建并启动新线程(修改flag的线程)
new Thread(() -> {
try {
Thread.sleep(100); // 延迟100ms,确保主线程先进入循环
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true; // 修改flag的值
System.out.println("修改停止标志为true..."); // 预期输出修改后的值
}).start();
// 主线程执行foo方法
foo();
}
static void foo() {
int i = 0;
// 无限循环检查flag状态
// 问题:主线程可能永远看不到新线程对flag的修改
while (!flag) {
i++; // 自增计数器(理论上会持续增长)
}
// 当flag变为true时输出结果(实际可能无法到达此处)
System.out.println("已停止... 计数:" + i);
}
}
运行结果
原因:
变量未声明为 volatile
在java中,每个线程会从主内存复制变量到工作内存,如果不使用volatile进行操作,线程对变量的感知可能只停留在工作内存中,其他线程无法立即感知到变化。
主线程视角:
flag
的初始值为false
,主线程的while
循环会持续读取本地内存中的false
。新线程视角:修改
flag
为true
后,仅更新了新线程的本地内存,未同步到主内存,主线程无法感知。
JIT 编译器的优化逻辑
JIT 编译器在运行时对热点代码(频繁执行的代码)进行优化,目标是提升执行效率。在该案例中,主线程的 while (!flag)
循环是典型的热点代码。
热点代码检测
触发条件:当
while (!flag)
循环被频繁执行(如达到 10,000 次),JIT 会将其标记为“热点代码”。优化目标:减少循环中的冗余操作(如重复读取
flag
)。
循环不变量传播
优化逻辑: JIT 分析代码后发现,
flag
在循环体内未被修改,且未受同步机制保护(如volatile
或锁)。 因此,JIT 认为flag
的值在循环过程中是恒定的(即使实际可能被其他线程修改)。最终优化:将循环条件替换为
true
,彻底跳过flag
的检查。
JIT 可能执行以下优化:
上述代码
while (!flag) { i++; }
在很短的时间内,这个代码执行的次数太多了,当达到了一个阈值,JIT就会优化此代码,如下:
while (true) { i++; }
当把代码优化成这样子以后,即使
flag
变量改变为了false
也依然停止不了循环
解决方案:
1.在程序运行的时候加入vm参数-Xint
表示禁用即时编辑器,不推荐,得不偿失(其他程序还要使用)
2.在修饰stop
变量的时候加上volatile
,表示当前代码禁用了即时编辑器,问题就可以解决,代码如下:
static volatile boolean stop = false;
禁止进行指令重排序
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
注解@Actor保证方法内的代码在同一个线程下执行
为什么不能是volatile x 呢?
所以,现在我们就可以总结一个volatile使用的小妙招:
写变量让volatile修饰的变量的在代码最后位置
读变量让volatile修饰的变量的在代码最开始位置