关于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值。
至此,真相大白了!

全部评论

相关推荐

11-18 09:44
Java
小白也想要offer:简历别放洋屁,搞不还还放错了,当然你投外企除外,以上纯属个人观点
点赞 评论 收藏
分享
球球别再泡了:坏,我单9要了14
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务