引言

在并发编程时,会碰到一个难题:即一个操作A的结果对另一个操作B可见,即多线程变量可见性问题。
解决方法就是提出了happens-before概念,即一个操作A与另一个操作B存在happens-before关系。

定义:

《Time,Clocks and the Ordering of Events in a Distributed System》点击查看论文。

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

前提:操作A happens-before 操作B。
对于第一条,编码时,A操作在B操作之前,则执行顺序就是A之后B。
对于第二条,如果重排序后,虽然执行顺序不是A到B,但是最终A的结果对B可见,则允许这种重排序。

规则:

  1. 程序次序规则:
    一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作,这个规则只对单线程有效,在多线程环境下无法保证正确性。
  2. 锁定规则:
    不管单线程多线程,一个unLock操作先行发生于后面对同一个锁的lock操作。
  3. volatile变量规则:
    它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后另一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。
  4. 传递规则:
    如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:
    假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。即:调用start方法时,会将start方法之前所有操作的结果同步到主内存中,新线程创建好后,需要从主内存获取数据。这样在start方法调用之前的所有操作结果对于新创建的线程都是可见的。
  6. 线程中断规则:
    对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  7. 线程终结规则:
    线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:
    一个对象的初始化完成先行发生于他的finalize()方法的开始;

Happens-Before原则到底是如何解决变量间可见性问题的?

我们已经知道,导致多线程间可见性问题的两个罪魁祸首是CPU缓存重排序。那么如果要保证多个线程间共享的变量对每个线程都及时可见,一种极端的做法就是禁止使用所有的重排序和CPU缓存。即关闭所有的编译器、操作系统和处理器的优化,所有指令顺序全部按照程序代码书写的顺序执行。去掉CPU高速缓存,让CPU的每次读写操作都直接与主存交互。
当然,上面的这种极端方案是绝对不可取的,因为这会极大影响处理器的计算性能,并且对于那些非多线程共享的变量是不公平的。
重排序CPU高速缓存有利于计算机性能的提高,但却对多CPU处理的一致性带来了影响。为了解决这个矛盾,我们可以采取一种折中的办法。我们用分割线把整个程序划分成几个程序块,在每个程序块内部的指令是可以重排序的,但是分割线上的指令与程序块的其它指令之间是不可以重排序的。在一个程序块内部,CPU不用每次都与主内存进行交互,只需要在CPU缓存中执行读写操作即可,但是当程序执行到分割线处,CPU必须将执行结果同步到主内存或从主内存读取最新的变量值。那么,Happens-Before规则就是定义了这些程序块的分割线。下图展示了一个使用锁定原则作为分割线的例子:
happens-before.jpg

如图所示,这里的unlock M和lock M就是划分程序的分割线。在这里,红色区域和绿色区域的代码内部是可以进行重排序的,但是unlock和lock操作是不能与它们进行重排序的。即第一个图中的红色部分必须要在unlock M指令之前全部执行完,第二个图中的绿色部分必须全部在lock M指令之后执行。并且在第一个图中的unlock M指令处,红色部分的执行结果要全部刷新到主存中,在第二个图中的lock M指令处,绿色部分用到的变量都要从主存中重新读取。
在程序中加入分割线将其划分成多个程序块,虽然在程序块内部代码仍然可能被重排序,但是保证了程序代码在宏观上是有序的。并且可以确保在分割线处,CPU一定会和主内存进行交互。Happens-Before原则就是定义了程序中什么样的代码可以作为分隔线。并且无论是哪条Happens-Before原则,它们所产生分割线的作用都是相同的。

参考:
https://www.cnblogs.com/chenssy/p/6393321.html
https://segmentfault.com/a/1190000011458941
https://blog.csdn.net/liu_dong_liang/article/details/80391040