java内存模型之:一文说清java线程的Happens-before

1. 前言

学习happens-before的目的不是只限于知道这些规则的存在,而是要进一步知道如何实现和维护这些happens-before关系,在代码中加以注意。html

Happens-before 规则是从java代码设计层面保证有序性和可见性的机制。本文将会以图示、样例代码和解释相结合的方式,力图阐述清楚happens-before的原理,为理解如何保证线程安全性打下扎实的基础。java

关于happens-before关系,java语言说明中有以下的描述:编程

Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second. -- Java Language Specification Ch 17.4.5. Happens-before Order

java代码编译成计算机指令后,计算机在执行指令时会进行必定程度的重排序。happens-before规则至关于java设计者和java开发者之间的约定,屏蔽了计算机底层的细节,从java层面和开发者创建约定,约定的内容是以下这样:安全

java语言的设计者创建了一套happens-before规则,若是开发者在相应的场景下确保两个操做之间听从了happens-before规则,那java会为开发者保证,听从了happens-before规则的多线程操做具备实际的happens-before关系,进而能够保证共享变量的可见性多线程

这些happenns-before关系大多须要开发者本身保证,而于此同时,java内置就已经实现了一系列happens-before关系,这些关happens-before 关系自然存在,而不须要开发者自行维护。并发

警戒误区

网上绝大多数的文章在描述happens-before关系时,关于已经存在仍是须要开发者留心维护这个点上上没有说清楚。
官方文档所描述的happens-before并不是陈述句,不是在表述一个客观事实,而是一个应该补上一个should,完整的语义应该为: Action A should have a happens-before relationship with action B, in order to ensure result of action A is visible to action B.
若是咱们把须要咱们花气力维护的happens-before关系当成了自然存在的关系,例如,这样的一条规则:“* A write to a volatile field happens-before every subsequent read of that field.”,这是一条须要开发者维护的规则,若是开发者直接把这条规则当成了固有事实,不管怎样的代码顺序均可以保证“对volatile变量的写必定发生在读以前”,那么线程安全就彻底得不到保障了。
所以,后文对于happens-before介绍将主要分为两类:即已经存在开发者自行保证oracle

2. Happens-before规则

Happens-before关系可传递性

在学习下面的happens-before规则以前,咱们须要了解到的一个已有规则是,happens-before关系具备可传递性。用hb(a,b)表示a happens-before b,而若是 hb(b,c),则咱们能够得知hb(a,c)。可传递性这一固有特性将帮助咱们顺利理解后面的诸多规则。app

已经存在(须要了解)

这一部分happens-before规则已经由java语言设计者实现,故对于开发者而言它们是固有存在的事实,了解他们有助于咱们清楚代码为何能够保证可见性,同时在复杂的状况有分析可见性问题的能力。ide

1. 单线程规则

image.png
咱们,都知道,在单线程内部,对于开发者而言,能够确保写在前面的操做会happens-before以后的操做。尽管计算机底层的指令和java代码的顺序会有比较大的差别,但其中的细节jdk会去处理,java能够向开发者保证,单线程全部前面代码的操做结果对于后面代码是可见的。对于这一条规则无需赘述,绝大多数开发者在编码的第一天都是默认了这个事实。而这一规则也是其余全部happens-before规则成立的基石。学习

2. Thread start规则

image.png
线程启动规则指出,线程A中启动线程B,那么线程A中在调用ThreadB.start()方法happens-before线程B 中的全部操做。因为可传递性的存在,咱们天然也能够得知,A线程中调用ThreadB.start()以前的全部操做均happens-before B线程的全部操做,即A线程在调用ThreadB.start()以前的操做结果对B线程可见。

3. Thread join规则

image.png
该规则指出,线程B中的全部操做happens-before线程B的join()方法。同理,根据可传递性,咱们也能够肯定,线程B中的全部操做如statement1 happens-before 线程A的statement2。

开发者自行保证(须要了解并清楚如何实现)

这一部分的规则须要开发者额外关注,由于java语言没有内置机制保证这些happens-before规则,要实现这些场景下的可见性,须要开发者自行保证这一关系的存在。若是缺乏这些关系,则java没法保证一个线程的操做结果对另外一个线程的可见性。

4. volatile变量规则

若是但愿A线程的对volatile变量的修改对B线程可见,那么A线程对volatile的变量的写应该happens-before B线程对这个volatile变量的读。

即若是保证线程A对一个volatile变量的写发生在另外一个线程B对于这个变量的读以前,在A对这个变量的操做对B可见。
下面经过一个简单的代码样例验证这一规则。

public class TestVolatileHappensBefore {
    static volatile int shared = 0;

    static class WriteTask implements Runnable {
        @Override
        public void run() {
            shared = 5; //1. write to the volatile shared variable
        }
    }

    static class ReadTaslk implements Runnable {
        @Override
        public void run() {
            System.out.println(shared);//2. read to the volatile shared variable
        }
    }

    public static void main (String[] args) {
        Thread tWrite = new Thread(new WriteTask());
        Thread tRead = new Thread(new ReadTaslk());
        tWrite.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            //just wait to ensure order
        }
        tRead.start();

    }
}

这段代码的运行结果显而易见,会打印shared的值为5。由于听从了volatile变量的happens-before规则,故注释中1的操做结果对2可见。若是仅展现正例则没法证实这一规则存在的必要性。而下面的这个反例则能够证实咱们必须确保这一 happens-before 关系。

public static void main (String[] args) {
       Thread tWrite = new Thread(new WriteTask());
       Thread tRead = new Thread(new ReadTaslk());
       tRead.start();
       tWrite.start();

   }

反面例子中仅更改了对于volatile变量读写线程的开始顺序,即咱们没有严格遵照volatile变量的happens-before规则,那这样的代码运行结果会如何呢?这里附上数次运行结果的截图:
image.png

image.png
从结果能够看出,在短短数次的重复运行中,shared变量的值有时为初始值0,有时为被写进程修改后的值5。也就是更新后的shared变量的值没法保证其对其余线程的可见性。故若是须要保证volatile变量的可见性,咱们须要知足volatile变量的happens-before规则。

5. 锁操做规则(synchronized及实现了Lock接口实现)

若是A线程是解锁操做,以后另外一个B线程执行加锁操做,若是但愿A的操做结果对B可见,那么 A应该happens-before B

一样经过一段样例代码展现该规则的正面案例,依然是线程先读后写的状况,同步机制依赖显示锁ReentrantLock:

public class TestLockHappensBefore {
    static int a = 0;
    static ReentrantLock lock = new ReentrantLock();

    public static void modify1() {
        lock.lock();
        try {
            System.out.println("a= " + a + " with " + Thread.currentThread().getName());
        } finally {
            a = 10;
            lock.unlock();
        }
    }

    public static void modify2() {
        lock.lock();
        try {
            System.out.println("a= " + a + " with " + Thread.currentThread().getName());
        } finally {
            a = 5;
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Thread thread0 = new Thread() {
            @Override
            public void run() {
                modify1();
            }
        };
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                modify2();
            }
        };

        thread0.start();
        thread1.start();
    }
}

thread0打印当前a的值并将a赋值为10,thread1一样打印当前a的值并将其赋值为5。

由于thread0 符合happens-before thread1的规则,因此thread0对变量a的修改对thread1可见,故运行结果为如图:
image.png

而若是将启动顺序倒置

thread1.start();
thread0.start();

则关系变成了 thread1 happens-before thread0,则thread1对a的修改对thread0可见,运行结果如图:
image.png
在这个样例代码中不存在关系的违反,但知足不一样的happens-before关系,根据规则,则会有不一样的结果,这须要开发者在开发过程当中留心观察,理清逻辑。

6. 各式各样的组合场景下的happens-before规则

这就是一个无穷无尽的话题了,在组合的场景下,只有符合了happens-before的线程两两之间可以保证可见性,而不必定能够保证全部线程的互相可见性,要处理好这种状况则须要开发者对
各类场景的happens-before规则烂熟于胸,利用好规则的可传递性,梳理清楚线程交互逻辑,才有可能处理好全部线程间的可见性问题。

一个小彩蛋
笔者在撰写样例代码的时候就遇到了这种组合的状况,happens-before关系并无造成传递链,故出现了意想不到的结果,仔细分析才得了缘由,与各位简单分享一下。代码以下:

public class LockUnexpectedHappensBefore {
    static int a = 0;
    public static synchronized void read() {
        System.out.println(a);
    }
    public static synchronized void write() {
        a = 5;
    }

    public static void main(String[] args) {
        Thread write = new Thread(){
            public void run() {
                write();
            }
        };
        write.start();
        read();
    }
}

 看到这个代码,不知道各位认为打印出来的a的值会是多少,我一开始下意识认为必定是5,而后结果是初始值0。
之因此会有这样的结果,是由于尽管write 线程的start方法 happens-before 主线程调用read()方法,但由于建立线程和线程执行须要时间,write线程的run()方法并不被保证 happens-before 主线程的read()方法。
在这种状况下write线程的unlcok不被保证 happens-before 于 主线程的lock,主线程天然无法看到write线程的操做结果。而若是咱们想要read的结果打印a = 5,方法也很简单,运用咱们以前的几个固有的happens-before规则。
例如,应用 thread join规则,将代码改成:

write.start();
   write.join();
   read();

则write线程和主线程知足happens-before规则,具备实际的happens-before关系,在结合happens-before关系的传递性,happens-before关系从符合thread jion规则传递到符合锁的规则,故write线程的修改必定对主线程可见。仅仅是这样一个简单的样例尚且容易出错,在更复杂的开发场景下,要想处理好可见性问题,只能靠开发人员本身不断实践和总结,才能获得真知。

引用

Java Language Specification
java并发核心编程78讲
Java - Understanding Happens-before relationship

相关文章
相关标签/搜索