关于Java内存可见性的探究实验遇到的意外和happens-before
java内存模型(JMM)学习过后,一直没有找机会进行实践,于是抽时间写了一个程序验证JVM内存模型中常见的线程不可见问题
程序如下:
package com.bestqiang.thread.JUC;
/** * @author BestQiang */
public class TestStatic {
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "开始了!。。。。");
Test test = new Test();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "开始了!。。。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1开始修改了!");
test.add();
System.out.println(Thread.currentThread().getName() + ":" + test.getA());
}, "线程1").start();
System.out.println(test.getA());
while (test.getA() == 0) {
}
System.out.println(Thread.currentThread().getName() + ":" + "内存可见性验证失败");
}
}
class Test {
int a = 0;
public void add() {
this.a = 60;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
在程序中,在线程1中进行Thread.sleep()操作,确保在更改变量前main线程已经冲主内存获取到了变量a的值。然后在线程1对值a进行修改后,刷新到主内存,主线程并没有从主内存重新获取,内存不可见问题验证成功,运行结果如下:
线程一直进入等待。
然而,我想再做点事情,让内存不可见性更明显,就在while循环中加入了一条输出语句,程序如下(仅仅加了一条语句哦),但是导致了截然不同的结果。
package com.bestqiang.thread.JUC;
/** * @author BestQiang */
public class TestStatic {
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "开始了!。。。。");
Test test = new Test();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "开始了!。。。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1开始修改了!");
test.add();
System.out.println(Thread.currentThread().getName() + ":" + test.getA());
}, "线程1").start();
while (test.getA() == 0) {
System.out.println("main线程值未更改");
}
System.out.println(Thread.currentThread().getName() + ":" + "内存可见性验证失败");
}
}
class Test {
int a = 0;
public void add() {
this.a = 60;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
程序运行结束,不再进行死循环,内存变得可见了。
这种情况弄得我一脸懵,后来再网上查,没查到,求助各路大神,终于搞清楚了。
原因是 happens-before
https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5?tdsourcetag=s_pctim_aiomsg
happens-before字面翻译过来就是先行发生,A happens-before B 就是A先行发生于B?
不准确!在Java内存模型中,happens-before
应该翻译成:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。我们再来看看为什么需要这几条规则?
因为我们现在电脑都是多CPU,并且都有缓存,导致多线程直接的可见性问题。
所以为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。编译器还会优化我们的语句,所以等于是给了编译器优化的约束。不能让它优化的不知道东南西北了!
关于 happens-before,有以下8条规则:
- 单线程Happens-Before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
- 锁的Happens-Before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的Happens-Before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
- Happens-Before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
- 线程启动的Happens-Before原则:同一个线程的start方法happen-before此线程的其它方法。
- 线程中断的Happens-Before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的Happens-Before原则:线程中的所有操作都happen-before线程的终止检测。
- 对象创建的Happens-Before原则:一个对象的初始化完成先于他的finalize方法调用。
详细请看:https://segmentfault.com/a/1190000011458941
那么,问题来了,上面的第二个程序,触发了这些规则中的哪一个呢?是 锁的Happens-Before原则!但是,只是加了一个输出语句,并没有加锁呀?No!点开println的源代码,告诉我,你发现了什么?
没错,里面加了同步锁,真相大白了,里面加了锁,所以由于happens-before原则,在获取锁时,B线程会使自己CPU的缓存失效,重新从主内存中读取变量的值。这样,线程1中的操作结果就会被主线程感知到了,从主内存中获取了最新的a值。
至此,真相大白了!