happens-before理解和应用

double

记录一下

为什么要有一个 happens-before 的原则?

我们都知道 cpu 运行很快,而读取系统内存对于 cpu 而言有点慢了,在读取系统内存过程中 cpu 一直闲着(没数据可运行),这对资源来说是极大的浪费,所以慢慢的 cpu 演变成了多级 cache 结构,cpu 在读 cache 的速度比读内存块了 n 倍。

当线程在执行时,会保存临界资源的副本到私有 work memory 中,这个 memory 是在 cache 中的,当修改这个临界资源时会更新 work memory 但并不一定立刻刷到主存中,那么什么时候应该刷到主存中呐?什么时候和其他副本同步?

而且编译器为了提高指令执行效率,是可以对指令重排序的,重排序后指令的执行顺序不一样,有可能线程2读取某个变量时,线程1还未进行写入操作,这就是线程可见性的来源。

总结:happens-before 决定着什么时候变量操作对你可见

定义

happens-before 字面翻译过来就是先行发生,A happens-before B 就是A先行发生于B?但是在Java内存模型中是不准确的!它有以下2个原则:

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序要排在第二个操作之前
  • 两个操作之间存在 happens-before 关系,并不意味着 java 平台的具体实现必须按照 happens-before 关系指定的顺序来执行。如果重排序后的执行结构,与按 happens-before 关系来执行的结果一致,那么这种重排序并不非法(JMM允许这种排序)

规则

1)、程序次序规则
一个线程中的每一个操作,happens-before 于该线程中的任意后续操作。

在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作 happens-before(时间上)后执行的操作。

2)管理锁定规则

一个 unlock 操作 happen-before 后面(时间上的先后顺序,下同)对同一个锁的 lock 操作。锁可以让临界区互斥执行,还可以让释放锁的线程向获取锁的线程发送消息,当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存读取共享变量。

如果线程1解锁了 monitor a, 接着线程2锁定了a, 那么线程1解锁a之前的写操作都对线程2可见,锁的语义决定了临界区代码的执行具有原子性。

3)volatile变量规则

对一个 volatile 变量的写操作 happens-before 后面对该变量的读操作。在编译时会产生 LoadLoad 屏障用来禁止处理器的普通读重排序

如果线程1写入了 volatile 变量 v(临界资源),接着线程2读取了v, 那么线程1写入v及之前的写操作都对线程2可见

4)线程启动规则

Thread对象的start() 方法 happens-before 此线程的每一个动作。

假定线程A在执行过程中,通过执行 ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B是可见的,PS: 线程B启动之后,线程A在对变量修改线程B未必可见。

5)线程终止规则

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

线程T1写入的所有变量,在任意其他线程T2调用T1.join()、t1.isAlive() 成功返回后,都对t2可见

6)线程中断规则

对线程 interrupt() 方法的调用 happen-before 发生于被中断线程的代码检测到中断时事件的发生。

线程t1写入的所有变量,调用 Thread.interrupt(),被打算的线程t2,可以看到 t1 的全部操作

7)对象总结规则

一个对象的初始化完成(构造函数执行结束)happen-before 它的 finalize() 方法的开始。

对象调用 finalize() 方法时,对象初始化完成的任意操作,同步到系统内存和全部 cache 中

8)传递性

如果操作 A happens-before 操作B,操作B happens-before 操作C,那么可以得出 A happens-before 操作 C。

A h-b B, B h-b C 那么可以得到 A h-b C

背后的道理

happens-before 本质是顺序,重点是跨越内存栅栏

在程序运行过程中,所有的变量都会保存在寄存器或者 cache 中,然后才会被拷贝到系统内存中以跨越内存栅栏,此种跨越序列或者顺序称为 happens-before。
通常情况下,写操作必须要happens-before读操作,即写线程需要在所有读线程跨越内存栅栏之前完成自己的跨越动作,其所做的变更才能对其他线程可见。

as-if-serial

所有的动作(Action)都可以为了优化而被重排序,但是在单线程内必须保证它们重排序后的结果和程序代码本身的应有结果是一致的

区别

  • as-if-serial: 保证单线程内程序的执行结果不被改变
  • happens-before: 保证正确同步的多线程程序的执行结果不被改变

as-if-serial语义和 happens-before这么做的目的都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

内存屏障

内存屏障也叫内存栅栏是一种CPU指令,用来控制特定条件下的重排序和内存可见性问题。java编译也会根据内存屏障规则来禁止重排序。
内存屏障可以被分为以下几种类型:

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见,它的开销是四种屏障中最大的。

扩展阅读