一、问题
一下代码经测试,打开注释行,子线程就不会陷入while死循环了,为什么呢
public class VolatileTest3 { // b使用volatile修饰 public static volatile long b = 0; //消除缓存行的影响 public static long a1,a2,a3,a4,a5,a6,a7,a8; // c不使用volatile修饰 public static long c = 0; public static void main(String[] args) throws InterruptedException { new Thread(()->{ while (c == 0) { //long x = b; } System.out.println("c=" + c); }).start(); Thread.sleep(100); b = 1; c = 1; } }
可以理解为:如果不加volatile,java编程语言的java memory model允许一个线程读到另一个线程任何一次写进去的值(可以是初值0也可以是主线程写入的1),只要不是happens-after它的就可以。但这个程序两个线程没有任何同步,所以没有任何happens-before关系。所以,就算主线程写,另一个线程永远读到c == 0,也是允许的。只要允许,你看到的“程序永远退不出去”就是合理的结果。至于为什么会出现这种现象,你暂且认为是巧合吧,反正这是《Java语言标准》允许的,JVM没做错什么。
但是一旦加上volatile,所有线程对c的读写操作就构成一个序列。因为main早晚会执行完,所以早晚会又一个对c的写操作,写入1。由于new thread会不断读c,早晚会有一次读happens after那个往c里写1的操作。对于volatile变量来说,写之后的读都能看到那个写的值“1”。所以那个new thread早晚可以看到c == 1。
或者可以理解为:共享变量c被两个线程读写,cpu缓存行存在两份值,如果不加volatile则更新过后的c的值不知道什么时候会更新到主存,加了volatile后会立即同步到主存,缓存行无效,另一个线程会重新从主存加载新值到cpu缓存行。
二、关于happens-before原则
Java内存模型中的happens-before是什么?为什么会有这东西的存在?
其实我们学习的初期或者时间很急迫的时候我们都是死记硬背,没有理解这东西背后的含义和为什么需要这东西。没办法比如项目赶,一个新东西肯定是上手先,但是等我们空下来回过头来,我们还是需要去理解这些知识,只有这样我才能深刻的记住,并且运用熟练。
happens-before字面翻译过来就是先行发生,A happens-before B 就是A先行发生于B?
不准确!在Java内存模型中,happens-before 应该翻译成:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。
我们再来看看为什么需要这几条规则?
因为我们现在电脑都是多CPU,并且都有缓存,导致多线程直接的可见性问题。详情可以看我之前的文章面试官:你知道并发Bug的源头是什么吗?
所以为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。编译器还会优化我们的语句,所以等于是给了编译器优化的约束。不能让它优化的不知道东南西北了!
咱们来看看这几条规则
程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
传递规则:这个简单的,就是happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。