问题引入
代码
目前有如下两个线程,根据代码逻辑可以得知,当 t2 将 isQuit修改为 true 后,t1 中的 while 应该立刻结束,随后 t1 线程退出。
public class Main {
static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} while(isQuit){
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(()->{
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
isQuit = true;
});
t1.start();
t2.start();
t1.join();
t2.join();
}}
分析
然而实际的运行结果却并没有输出"t1线程结束",这是为何?
首先初步分析,在 Java 的程序中有“两个内存”,一个是主内存,一个是工作内存,isQuit 本身是存储在主内存中的,而编译器为了加快代码的执行速度,将 isQuit 放入了工作内存;接下来 t2 线程修改了主内存中的 isQuit 。然而不会影响到工作内存中的 isQuit,因为编译器优化的问题,t1 不会再去主内存中读取 isQuit 的值,所以 t1 线程一直不会停止。
注:
- work memory:“工作内存”不是我们平常说的“内存”,它包含了CPU的寄存器和缓存
- main memory: “主内存”才是平常说的“内存”
- 造成这样的原因,是因为 Java 本身是跨平台语言,不同计算机不同操作系统内的组成也不同。因此对“内存”的命名方式也与我们传统认知不同。
原因
实际上这是由编译器进行代码优化造成的 bug,编译器本身是“好心的”,通过优化可以进一步提高代码的执行效率,优化能够在保持代码逻辑不变的情况下,调整代码生成的内容。如果代码是单线程的,那么往往不会出现什么问题,但是如果代码是多线程的,优化后可能会出现问题,优化后的逻辑和优化前的逻辑可能就不一样了。
如图所示,t1 线程一直在运行并没有退出,因此“t1线程结束”也没有输出。
在这种情况下,我们可以自己添加代码,来防止编译器对代码进行优化,保证内存的可见性。
保证内存可见性
volatile 关键字
通过在代码中添加 volatile 可以保证内存可见性,这样编译器就不会进行优化了
static volatile boolean isQuit = false;
对变量添加 volatile,其能够告诉编译器,不要对此处进行优化,因而 t1 线程就能够正常退出了。
如图所示,线程正常结束。
synchronized 关键字
之前我们提到过,synchronized能够保证线程安全(原子性),但是其也能够保证内存可见性,因此也可以通过为代码块“上锁”的方式来保证内存可见性。
补充:虽然 synchronized 和 volatile 都能保证内存可见性,但是volatile不能保证原子性。
局限性
优化后无论是性能、执行效率上都会比优化前快很多,这也是编译器冒着这么大的风险进行优化的原因。因此也不要滥用 synchronized 和 volatile,因为滥用会造成代码的执行效率下降,系统的并发程度下降。编译器冒着这么大的风险进行优化,也是有原因的,