线程可见性问题?还是编译优化问题?
代码还原
首先我们看下下面的代码
public class VolatileTest {public static int count = 1;public static void setCount() {while (count == 1 ) {}System.out.println("setCount 方法执行");}public static void main(String[] args) throws InterruptedException {new Thread(() -> setCount()).start();Thread.sleep(100);count = 2;}
}
上面代码的逻辑非常简单,这里就不在多赘述了。
使用JVM1.8 虚拟机参数是默认参数,代码执行之后,发现程序结束不了
。
问题分析观点
1: 线程间可见性问题
有人说是线程可见性问题。变量count的值被修改了,子线程执行setCount()方法的时候,count的值一直不可见的。给count加上volatile关键字就能解决问题。代码如下
public class VolatileTest {public static volatile int count = 1;public static void setCount() {while (count == 1 ) {}System.out.println("setCount 方法执行");}public static void main(String[] args) throws InterruptedException {new Thread(() -> setCount()).start();Thread.sleep(100);count = 2;}
}
执行完成之后,你会发现,问题确实解决了。
但这真的是线程可见性的问题吗?
我们思考一下,count的值是类成员变量,一个线程修改count值之后,肯定会刷入到内存中的。那也就是说,在不加volatile的前提下,如果我把代码中 Thread.sleep()的方法参数中的时间调大一些,应该能够将count的最新值刷入到内存中,setCount()方法可见。试了之后,发现无论调多大时间,程序都终止不了。
2:编译器优化问题
我们知道,JVM会对我们的代码进行编译优化,我们来验证一下,是不是编译器优化的问题。
JVM采用分层编译, JVM 将执行状态分成了 5 个层次:
0层:解释执行,用解释器将字节码翻译为机器码
1层:使用 C1 即时编译器编译执行(不带 profiling)
2层:使用 C1 即时编译器编译执行(带基本的profiling)
3层:使用 C1 即时编译器编译执行(带完全的profiling)
4层:使用 C2 即时编译器编译执行。
也就是说,只需要验证一下C1和C2编译之后执行的代码是否有问题即可
情景1: 只用C1编译执行,禁止C2编译执行
JVM参数新增 -XX:TieredStopAtLevel=3
此参数的作用是仅启用C1编译器,同时禁用C2编译器
执行之后,你会发现,不加volatile关键字,程序也能结束。
情景2: 只用C2编译执行,禁止C1编译执行
JVM参数新增 -XX:TieredStopAtLevel=4
此参数JVM将使用C2编译器进行编译。
执行之后,你会发现,程序结束不了。
结论
导致代码执行问题的根本问题是 编译器优化
问题,并不是线程间的可见性问题
。这里这是证明了问题发生在C2编译器编译的地方。实际上我们可以看下JVM执行的C2编译之后指令即可发现问题的根本所在。这里有兴趣的读者可以本地编译一下JVM源码,将执行的指令打印出来,进行日志分析即可。