JMM之先行发生原则(happens-before)详解

1、概述

在JMM规范下,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happers-before(先行发生)原则。

例如

int x = 10 ;

int y = x;

这两行代码中第二个操作 y=x ,因为需要将x值赋值给y,所以第一行代码的结果需要对第二行代码可见。也就是第一行代码happers-before第二行代码。

有些朋友看了可能觉得,作者说的不是废话嘛,这两行代码肯定是先执行第一行再执行第二行。答案肯定是的,因为y的值依赖x的值,这属于JMM规范happens-before原则内的,所以会按照代码编写顺序执行。假如没有happens-before原则,则y的值可能就不会等于10,可见happens-before原则的重要性。

happens-before原则是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单的规则,解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。

那接下来我们就说说happens-before都有那些具体原则。

2、happens-before具体原则

  • 次序规则

一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作。

注意:这里是一个线程内,如果是多个线程这不属于happens-before原则

int x = 10;

int y = 20;

在一个线程内 在执行y=20 时,x肯定是10;但是如果在多线程下由于编译器优化,指令重排序 在执行y=20时,x可能等于0

  • 锁定规则

一个unlock操作先行发生于后面(这里的”后面“是指时间上的先后)对同一把锁的lock操作;

    static Lock lock = new ReentrantLock();

    public static void main(String[] args) {

        lock.lock();
        try {
            System.out.println("------锁定1");
        } finally {
            //解锁
            lock.unlock();
        }

        //再次锁定  第二次加锁操作肯定是发生在上一次解锁 之后
        lock.lock();
        try {
            System.out.println("------锁定2");
        } finally {
            lock.unlock();
        }

    }
  • volatile变量规则

对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的”后面“也是指时间上的先后。

咱们都知道被volatile修饰的变量,拥有JMM三大特性的可见性和有序性。对于同一个共享volatile变量,线程A修改了变量值,则其他线程立马就可以看到。


    static volatile Boolean flag = false;

    public static void main(String[] args) {

        new Thread(() -> {
            if (!flag) {
                System.out.println("--正常运行");
            }
            System.out.println("--终止");
        }, "t1").start();

        new Thread(() -> {
            //更改volatile 变量值,t1线程立马就能收到
            flag = true;
            System.out.println("终止线程t1");
        }, "t2").start();
    }

看到这里是不是就知道了,为啥 volatile 修饰变量,变量具有可见性和有序性,其实底层是happens-before原则制定的。

  • 传递性规则

如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A肯定先行发生于C。

  • 线程启动规则

Thread对象的start()方法先行发生于此线程中执行的所有代码。


    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            /**
             * 线程内操作 后执行
             */
            System.out.println("线程执行内容1");
            System.out.println("线程执行内容2");
        }, "t1");


        // start()方法先执行
        t1.start();

    }
  • 线程中断规则

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()或者isInterrupted()检测到是否有中断发生。


        Thread t1 = new Thread(() -> {
            //检查线程中断标识位是否被设置为true
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("---正常执行");
            }
            System.out.println("被中断");
        }, "t1");
        //线程1启动
        t1.start();


        new Thread(() -> {
            System.out.println("将t1 线程中断标识位设置为true");
            t1.interrupt();
        }, "t2").start();

---正常执行
.....
.....
---正常执行
---正常执行
---正常执行
---正常执行
将t1 线程中断标识位设置为true
---正常执行
被中断 

  • 线程终止规则

线程中的所有操作都先行发生于对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  • 对象终结规则

一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

这条规则指出,一个对象在被垃圾回收之前必须已经进过初始化,垃圾回收不可能也不能去回收一个根本不存在的对象。

3、总结

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序是可以正常执行的。

猜你喜欢

转载自blog.csdn.net/u011837804/article/details/130114697