本文转载自微信公众号“飞天小牛”,作者飞天小牛。转载本文请联系飞天小牛公众号。关于Happens-before,《Java 并发编程的艺术》一书是这样介绍的:Happens-before是JMM的核心概念。对于Java程序员来说,理解Happens-before是理解JMM的关键。《深入理解 Java 虚拟机 - 第 3 版》书中是这样介绍的:Happens-before是JMM的灵魂。是判断数据是否存在竞争,线程是否安全的一种非常有用的手段。我想这两句话足以说明Happens-before原则的重要性。那为什么Happens-before被称为JMM的核心和灵魂呢?它是这样诞生的。JMM设计者的问题及完美解决上一篇《千里》详细讲解了Java内存模型和原子性、可见性、顺序。我们已经了解了JMM及其三个属性。其实,从JMM设计者的角度来看,可见性和顺序其实是两个矛盾的点:一方面,对于程序员来说,我们希望内存模型易于理解,易于编程。为此,JMM设计者必须为程序员提供足够强的内存可见性保证,技术上称为“强内存模型”。另一方面,编译器和处理器希望内存模型对它们的约束尽可能少,这样它们就可以做尽可能多的优化(比如重新排序)来提高性能。并且处理器的约束要尽可能放宽,专业术语叫“弱内存模型”。对于这个问题,从JDK5开始,也就是在JSR-133内存模型中,终于给出了一个完美的解决方案,那就是Happens-before原则,Happens-before直译为“先发生”,《JSR-133:Java Memory Model and Thread Specification》Happens-before关系的定义如下:1)如果一个操作Happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,第一个操作的执行顺序排在第二位手术前。2)两个操作之间存在Happens-before关系并不意味着Java平台的具体实现一定要按照Happens-before关系规定的顺序执行。如果重排序后的执行结果和按照Happens-before关系执行的结果一致,那么这次重排序不违法(即JMM允许这次重排序)不难理解,第一个定义是JMM承诺一个程序员的强大记忆模型。从程序员的角度来看,Happens-before关系可以这样理解:如果AHappens-beforeB,那么JMM会向程序员保证A的操作结果对B是可见的,A的执行顺序是B之前。注意,这只是Java内存模型对程序员做出的保证!需要注意的是,与as-if-serial语义只能应用于单个线程不同,这里所说的A和B这两个操作可以在一个线程内执行,也可以在不同线程之间执行。也就是说,Happens-before提供跨线程的内存可见性保证。对于这第一个定义,我举个例子://下面的操作是在线程A中执行的,i=1;//a//下面的操作是在线程B中执行的,j=i;//b//以下操作在线程B中执行Executei=2inthreadC;//c假设线程A中的操作ahappens-before线程B中的操作b,那么我们可以确定操作b执行后,变量的值j一定等于1,得出这个结论有两个依据:一是根据Happens-before原则,操作a的结果对b是可见的,即“i=1”的结果可以是观察到的;另一种是线程C还没有运行,线程A操作结束后不会有其他线程修改变量i的值。现在考虑线程C,我们仍然维护一个Happens-beforeb,c出现在a和b的操作之间,但是c和b没有Happens-before关系,也就是说b不一定看到c的操作结果。那么b操作的结果,也就是j的值是不确定的,可能是1也可能是2,那么这段代码就不是线程安全的。再来看Happens-before的第二个定义,这是JMM对编译器和处理器弱内存模型的保证。给定足够的可操作空间,对编译器和处理器的重新排序施加某些约束。也就是说,JMM其实是在遵循一个基本原则:只要程序的执行结果不变(指单线程程序和正确同步的多线程程序),编译器和处理器就可以对其进行优化。JMM之所以这样做是因为:程序员不关心这两个操作是否真的重新排序,程序员关心的是执行结果不能改变。文字可能不太容易看懂。下面举例说明第二个定义:虽然两个操作之间存在Happens-before关系,但并不意味着Java平台的具体实现必须按照Happens-before关系来指定。要执行的顺序。inta=1;//Aintb=2;//Bintc=a+b;//C根据Happens-before规则(下文会介绍),上面代码中有3种Happens-before关系:1)AHappens-beforeB2)BHappens-beforeC3)AHappens-beforeC可以看出,三种Happens-before关系中,第二和第三个是必须的,而第一个是不必要的。也就是说,虽然AHappens-beforeB,但是A和B之间的重排序根本不会改变程序的执行结果,所以JMM允许编译器和处理器进行这种重排序。看下面JMM的设计图更直观:图片来源《Java 并发编程的艺术》其实可以这么简单的理解,是为了避免Java程序员为了理解而去学习复杂的重排序规则和这些规则的规则JMM提供的内存可见性保证对于具体的实现方式,JMM想出了这么一个简单易懂的Happens-before原则。Happens-before规则对应于一个或多个编译器和处理器的重新排序规则。这样,我们只需要了解Happens-before规则即可。之前会做。图片来源《Java 并发编程的艺术》8Happens-before规则《JSR-133:Java Memory Model and Thread Specification》定义了以下Happens-before规则,它们是JMM中“自然”的Happens-before关系。这些Happens-before关系已经存在,无需任何同步器的帮助,可以直接在代码中使用。如果两个操作之间的关系没有在这里列出,并且不能从下面的规则推导出来,那么它们就没有顺序保证,JVM可以随意重新排序:1)程序顺序规则:在一个线程中,根据控制流程顺序,写在前面的操作先发生(Happens-before)在写在后面的操作之前。注意,这里说的是控制流的顺序,而不是程序代码的顺序,因为必须考虑分支、循环等结构。这很容易理解,也符合我们的逻辑思维。比如我们上面给出的例子:synchronized(this){//这里自动加锁if(x<1){x=1;}}//这里自动解锁根据程序顺序规则,有3个发生在abovecode-beforerelationship:AHappens-beforeBBHappens-beforeCAHappens-beforeC2)监控锁规则(MonitorLockRule):一个解锁操作发生在后续对同一个锁的锁操作之前。这里必须强调的是“同锁”,“后”是指时间上的先后顺序。这个规则其实是针对synchronized的。JVM并没有直接向用户开放加锁和解锁操作,而是提供了更高级的字节码指令monitorenter和monitorexit来隐式使用这两个操作。这两条字节码指令在Java代码中体现为一个同步块——synchronized。例如:synchronized(this){//这里自动加锁if(x<1){x=1;}}//这里根据monitor加锁规则自动解锁,假设x的初始值为10,线程A执行完代码块后,x的值会变成1,执行完锁自动释放。当线程B进入代码块时,可以看到线程A对x的写操作,即线程B可以看到x==1。3)易失性变量规则(VolatileVariableRule):对一个易失性变量的写操作发生在对这个变量的读操作之前,这里的“后”也是指时间上的先后顺序。这条规则是JDK1.5版本中volatile语义的增强,意义重大,依赖这条规则很容易获得可见性。例如:假设线程A执行writer()方法后,线程B执行reader()方法。根据程序顺序规则:1Happens-before2;3happens-before4.根据volatile变量的规则:2happens-before3.根据传递规则:1Happens-before3;1Happens-before4.也就是说,如果线程B读到了“flag==true”或者“inti=a”,那么线程A设置的“a=42”对于线程B是可见的。见下图:4)线程启动规则(ThreadStartRule):Thread对象的start()方法先于这个线程的每一个动作。例如主线程A启动子线程B后,子线程B在启动子线程B之前可以看到主线程的所有操作。5)线程终止规则(ThreadTerminationRule):所有操作在线程中首先发生在该线程的终止检测中。我们可以通过检查Thread对象的join()方法是否结束,Thread对象的isAlive()的返回值等方式来检测线程是否已经终止执行。6)线程中断规则(ThreadInterruptionRule):当被中断线程的代码检测到中断事件发生时,首先调用线程interrupt()方法,可以使用Thread对象的interrupted()方法检测是否有中断。7)对象终结规则(FinalizerRule):一个对象的初始化完成(构造函数执行结束)发生在它的finalize()方法开始之前。8)传递性:如果操作A发生在操作B之前,操作B发生在操作C之前,那么可以得出操作A发生在操作C之前。上面8中的“happens-beforeintime”和“happens-before”rules,时间的先后顺序也不断被提及,那么,“happens-beforeintime”和“happens-before(发生之前)”有什么区别呢?一个操作“happenfirstintime”是指这个操作会“首先发生”吗?一个操作“首先发生”是否可以推断出该操作必须“及时首先发生”?不幸的是,这两种推论都是站不住脚的。举两个例子来说明:privateintvalue=0;//线程A调用publiccvoidsetValue(intvalue){this.value=value;}//线程B调用publicintgetValue(){returnvalue;}假设有线程A和B,线程A首先(按时间顺序)调用setValue(1),然后线程B调用同一个对象的getValue(),那么线程B收到的返回值是多少呢?下面按照Happens-before的上述8条规则依次分析:由于这两个方法分别被线程A和B调用,不在同一个线程中,所以这里不适用程序顺序规则;既然没有synchronized同步块,加锁和解锁操作自然不会发生,所以monitor加锁规则在这里不适用;同样,volatile变量的规则,线程启动、终止、中断和对象终结的规则在这里完全不相关。由于没有适用的Happens-before规则,规则8也不具有传递性。因此,我们可以确定,虽然线程A在运行时间上领先于线程B,但不能说Ahappens-beforeB,即A线程运行B的结果不一定能看到。所以,这段代码是线程不安全的。解决这个问题也很简单?由于不满足Happens-before原则,我可以修改它使其满足。例如,将Getter/Setter方法修改为synchronized,这样就可以应用monitor锁规则;另一个例子是将value定义为一个volatile变量,这样就可以应用volatile变量规则等。这个例子演示了一个操作“在时间上最先发生”并不意味着该操作将“先于发生”。再看一个例子://下面的操作是在同一个线程中执行的inti=1;intj=2;假设这段代码中的两个赋值语句在同一个线程中,那么根据程序顺序规则,“inti=1”操作首先发生(Happens-before)在“intj=2”中,但是,记住第2个发生之前的定义?记住JMM其实遵守这样一个原则:只要不改变程序的执行结果(指单线程程序和正确同步的多线程程序),编译器和处理器就可以对其进行优化。因此,“intj=2”这句代码可能先被处理器执行,因为它不影响程序最终的运行结果。然后,这个例子演示了一个操作“先于发生”并不意味着该操作必须“及时先于发生”。这样,基于上面两个例子,我们可以得出这样一个结论:Happens-before原则和时间顺序之间基本没有因果关系。因此,我们在衡量并发安全问题时,尽量不要被时间顺序打乱。一切都必须遵循先发生后发生的原则。Happens-before和as-if-serial综上所述,我认为如果你理解了下面这句话,你也能理解Happens-before。这句话在上面出现过几次:JMM其实是遵循一个基本的原则,就是只要程序的执行结果不变(指的是单线程程序和正确同步的多线程程序),编译器和处理器可以优化他们想要的任何东西。再回顾一下as-if-serial的语义:无论怎么重新排序,单线程环境下程序的执行结果是无法改变的。你发现了吗?Happens-before关系和as-if-serial语义本质上是一回事,都是在不改变程序执行结果的情况下,尽可能提高程序执行的并行性。只是后者只能在单线程中使用,而前者可以在正确同步的多线程环境中使用:as-if-serial语义保证程序在单线程中的执行结果不被改变,而happens-before关系保证了正确的同步,多线程程序的执行结果不会改变。as-if-serial语义给编写单线程程序的程序员造成一种错觉:单线程程序按程序顺序执行。Happens-before关系为编写正确同步的多线程程序的程序员创造了一种错觉:正确同步的多线程程序按照Happens-before指定的顺序执行。参考《Java 并发编程的艺术》《深入理解 Java 虚拟机 - 第 3 版》
