关于happens-before规则的研究

来源:互联网 发布:淘宝网男士运动套装 编辑:程序博客网 时间:2024/05/16 07:42
  • 简介
  • JMM是什么?
  • 线程间的操作可见性
  • happens-before规则
  • 总结
  • Refer

简介###

Java使用新的JSR-133内存模型,JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

JMM是什么?

首先,Java是一门支持并发编程的语言,说的通俗点是一门支持多线程编程的语言。那么线程之间如何怎么通信的呢?一般有两种方法, 共享内存 和 消息传递 。Java采用的是共享内存的方式。

其次,当一个线程读/写一个变量时,这个读写操作真的会发生在内存吗? 不一定 。我们知道,为了提升性能,编译器和处理器都会进行一定的优化,而这些优化往往是我们看不见的。例如,在CPU和主内存中间有一个高速缓存,CPU在读写的时候先访问高速缓存,如果不能满足其次才会访问主内存。所以,当一个线程写一个变量时,这个写操作可能只发生在高速缓存中,而没有刷新到主内存。

如果只有一个线程执行或者CPU只有一个,这个行为并不重要。但在多线程及多CPU情况下,如果一个线程a正在CPU_1上执行,而同时有另外一个线程b在CPU_2上执行呢?a在CPU_1上写了一个变量可能只写到CPU_1的高速缓存中,那么线程b无论是访问CPU_2的缓存区还是访问主内存,都看不到线程a的写操作。

因此,为了清楚的定义 线程间的操作可见性 ,Java专家组制定了Java内存模型。这个模型制定了从宏观到微观的规则,宏观的规则让Java开发者能够不了解底层实现的情况下仍然可以方便地进行并发编程并且得到可预期的结果,微观的规则定义了上层的语义在编译器、处理器级别的行为。

线程间的操作可见性

上面提到了线程间的操作可见性,那么这个可见性问题具体是由于什么原因产生的呢?准确的来说,是由于存在以下这三种重排序:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级别的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存操作的重排序。由于处理器使用高速缓存,使得加载和存储操作看上去可能是在乱序执行。

第一种和第二种重排序相对容易理解,第三种是什么意思呢?

我们先来学习《深入理解Java内存模型》中的一个例子:

这里写图片描述

就是说,如果处理器A及处理器B的指令顺序如上所示,同时A和B并行执行,那么最终可能得到x = y = 0的结果。具体原因图示如下:

这里写图片描述

首先,处理器A和B把共享变量写入自己的缓冲区(A1和B1),然后从内存中读取另一个共享变量(A2和B2),最后才把自己写缓冲区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到x = y = 0的结果。

请注意, 以内存操作实际发生的顺序 来看,直到处理器A执行A3来刷新自己的写缓冲区,写操作A1才算真正执行了。虽然处理器A执行的顺序为:A1 -> A2,但实际内存操作实际发生的顺序却是:A2 -> A1。

因此,由于重排序的存在,线程间存在操作可见性问题。就是说,线程a先执行了某个操作,而线程b随后执行却看不到该操作的结果!

happens-before规则

为了解决线程间的操作可见性问题,JMM定义了一套happens-before规则,我们可以根据这套规则来编程从而得到可预期的结果。

1、程序顺序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。

2、管理锁定规则:一个unlock操作happen—before(时间上的先后顺序,下同)对同一个锁的lock操作。

3、volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。

4、传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

5、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。

6、线程终止规则:线程的所有操作都happen—before此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

7、线程中断规则:对线程interrupt()方法的调用happen—before于被中断线程的代码检测到中断事件的发生。

8、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。

前四条规则单独看上去没有什么厉害的地方,但是。。。如果综合运用起来就起到很大作用了。

我们还是来看个《深入理解Java内存模型》中的例子吧!

class VolatileExample {    int a = 0;    volatile boolean flag = false;    public void writer() {        a = 1;           //1        flag = true;     //2    }    public void reader() {        if (flag) {       //3            int i = a;    //4            ...        }    }}

假设线程A执行writer()方法 之后 ,线程B执行reader()方法,那么线程B执行4的时候一定能看到线程A写入的值吗?注意,a不是volatile变量。

答案是肯定的。因为根据happens-before规则,我们可以得到如下关系:

  • 根据程序顺序规则,1 happens-before 2;3 happens-before 4。

  • 根据volatile规则,2 happens-before 3。

  • 根据传递性规则, 1 happens-before 4 。

因此,综合运用程序顺序规则、volatile规则及传递性规则,我们可以得到1 happens-before 4,即线程B在执行4的时候一定能看到A写入的值。上述关系图示如下:

这里写图片描述

同样的,我们再来看一个锁规则的例子

class MonitorExample {    int a = 0;    public synchronized void writer() {    //1        a = 1;                            //2    }                                     //3    public synchronized  void reader() {   //4            int i = a;                    //5            ...    }                                     //6}

假设线程A执行writer()方法 之后 ,线程B执行reader()方法。那么根据happens-before规则,我们可以得到:

  • 根据程序顺序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6。

  • 根据监视器锁规则,3 happens-before 4。

  • 根据传递性规则, 2 happens-before 5 。

上述关系图示如下:

这里写图片描述

总结

happens-before的定义

happens-before的概念最初由Leslie Lamport在其一篇影响深远的论文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出。Leslie Lamport使用happens-before来定义分布式系统中事件之间的偏序关系(partial ordering)。Leslie Lamport在这篇论文中给出了一个分布式算法,该算法可以将该偏序关系扩展为某种全序关系。

JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。

《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义如下。

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

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

上面的1)是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!

上面的2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。

Refer

1、JMM的happens-before规则

2、多线程 happens-before规则

原创粉丝点击