在知乎上看到一个问题《java中volatile关键字的疑惑?》,引发了个人兴趣html
问题是这样的:java
1 package com.cc.test.volatileTest; 2 3 public class VolatileBarrierExample { 4 private static boolean stop = false; 5 6 public static void main(String[] args) throws InterruptedException { 7 Thread thread = new Thread(new Runnable() { 8 @Override 9 public void run() { 10 while (!stop) { 11 } 12 } 13 }); 14 15 thread.start(); 16 Thread.sleep(1000); 17 stop = true; 18 thread.join(); 19 } 20 }
这段代码的主要目的是:主线程修改非volatile类型的全局变量stop,子线程轮询stop,若是stop发生变更,则程序退出。编程
可是若是实际运行这段代码会形成死循环,程序没法正常退出。windows
若是对Java并发编程有必定的基础,应该已经知道这个现象是因为stop变量不是volatile的,主线程对stop的修改不必定能被子线程看到而引发的。架构
可是题主玩了个花样,额外定义了一个static类型的volatile变量i,在while循环中对i进行自增操做,代码以下所示:并发
1 package com.cc.test.volatileTest; 2 3 public class VolatileBarrierExample { 4 private static boolean stop = false; 5 private static volatile int i = 0; 6 7 public static void main(String[] args) throws InterruptedException { 8 Thread thread = new Thread(new Runnable() { 9 @Override 10 public void run() { 11 int i = 0; 12 while (!stop) { 13 i++; 14 } 15 } 16 }); 17 18 thread.start(); 19 Thread.sleep(1000); 20 stop = true; 21 thread.join(); 22 } 23 }
这段程序是能够在运行一秒后结束的,也就是说子线程对volatile类型变量i的读写,使非volatile类型变量stop的修改对于子线程是可见的!app
看起来使人感到困惑,可是实际上这个问题是不成立的。ide
先给出归纳性的答案:stop变量的可见性不管在哪一种场景中都没有获得保证。这两个场景中程序是否能正常退出,跟JVM实现与CPU架构有关,没有肯定性的答案。函数
下面从两个不一样的角度来分析优化
第一个场景就不谈了,即便在第二种场景里,虽然子线程中有对volatile类型变量i的读写+非volatile类型变量stop的读,可是主线程中只有对非volatile类型变量stop的写入,所以没法创建 (主线程对stop的写) happens-before于 (子线程对stop的读) 的关系。
也就是不能期望主线程对stop的写必定能被子线程看到。
虽然场景二在实际运行时程序依然正确终止了,可是这个只能算是运气好,若是换一种JVM实现或者换一种CPU架构,可能场景二也会陷入死循环。
能够设想这样的一个场景,主/子线程分别在core1/core2上运行,core1的cache中有stop的副本,core2的cache中有stop与i的副本,并且stop和i不在同一条cacheline里。
core1修改了stop变量,可是因为stop不是volatile的,这个改动能够只发生在core1的cache里,而被修改的cacheline理论上能够永远不刷回内存,这样core2上的子线程就永远也看不到stop的变化了。
因为run方法里的while循环会被执行不少次,因此必然会触发jit编译,下面来分析两种状况下jit编译后的结果(触发了屡次jit编译,只贴出最后一次C2等级jit编译后的结果)
如何查看JIT后的汇编码请参看个人这篇博文:《如何在windows平台下使用hsdis与jitwatch查看JIT后的汇编码》
ps. 回答首发于知乎,从新截图太麻烦,所以实际分析使用的Java源码与前面贴的代码略有不一样,不影响理解,会意便可。
若是把jit编译后的代码改写回来,大概是这个样子
1 if(!stop){ 2 while(true){ 3 i++; 4 } 5 }
很是明显的指令重排序,JVM以为每次循环都去访问非volatile类型的stop变量太浪费了,就只在函数执行之初访问一次stop,后续不管stop变量怎么变,都无论了。
第一种状况死循环就是这么来的。
从第一个红框开始看:
也就是说,每次循环都会去访问一次stop变量,最终访问到stop被修改后的新值(可是不能确保在全部JVM与全部CPU架构上都必定能访问到),致使循环结束。
这两种场景的区别主要在于第二种状况的循环中有对static volatile类型变量i的访问,致使jit编译时JVM没法作出激进的优化,是附加的效果。
涉及到内存可见性的问题,必定要用happens-before原则细致分析。由于你很难知道JVM在背后悄悄作了什么奇怪的优化。