首页 > 代码库 > 聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则

聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则

在前几篇将Java内存模型的那些事基本上把这个域底层的概念都解释清楚了,聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障 这篇分析了在X86平台下,volatile,synchronized, CAS操作都是基于Lock前缀的汇编指令来实现的,关于Lock指令有两个要点:

1. lock会锁总线,总线是互斥的,所以lock后面的写操作会写入缓存和内存,可以理解为在lock后面的写缓存和写内存这两个动作称为了一个原子操作。当总线被锁时,其他的CPU是无法使用总线的,也就让其他的读写都等待lock的释放

2. Lock写完后,发起它的CPU的缓存和内存都是最新值,其他CPU相关的缓存行都会invalidate,后续的读/写就会发生缓存不命中,从内存重新加载最新值。


这里有个隐含的点,我没找到具体的资料,但是按照很多资料的说法: volatile的写操作相当于释放锁,volatile的读操作相当于进入锁可以做下面的推断:

volatile操作的是一个变量,而锁保护的程序段中涉及到的变量可以是多个,既然两者的效果是一样的,那么很可能lock后面的写会让高速缓存/写缓存区的所有脏数据都刷新回主存。只有这样volatile在可见性方面和锁保护的程序段的可见性才是行为一致的。


理解这个很重要,因为和这篇讲的Happens-before传递性有关系。Happens-before刚看到的时候从语言上看很难理解,觉得是废话,但是它实际描述的问题其实是可见性的问题,顺带着有一些由于防止重排序而带来的有序性的问题。聊聊高并发(三十三)Java内存模型那些事(一)从一致性(Consistency)的角度理解Java内存模型 这篇说了,内存模型是一致性这个问题域里面的,一致性问题只涉及到了可见性和有序性这两种特性,不包含原子性,所以Happens-before实际上是一系列的一致性的约束,所以它涉及到了可见性和有序性的意思,但没有原子性的含义。


happens-before俗解 这篇文章已经写的很清楚了,我这边再结合上一篇内存屏障的一些概念锦上添花一下,进一步说明这个问题


下面这些Happens-before的规则是从JSR 133 (Java Memory Model) FAQ 摘出来的,一条条看

  • Each action in a thread happens before every action in that thread that comes later in the program‘s order.
  • 可以理解为对于单个线程来说,前面的写操作对后面都是可见的,这里肯定有人问那指令重排序之后怎么保证这点呢,我也有这个疑问,所以我理解的是如果这个写是同步的,那么对单线程来说,所有同步的写都是按照program order的,这个也是顺序一致性的第一层含义。要理解的是,Java在使用了同步手段之后,被同步保护的点都是保证顺序一致性的。因为同步的底层实现比如内存屏障 / lock都有防止重排序的含义


  • An unlock on a monitor happens before every subsequent lock on that same monitor.
  • 可以理解为一个锁的释放后它前面的写操作对后续进入同一个锁的线程可见,对锁来说这个太肯定了,释放时会lock cmpxchg一次,进入时会lock cmpxchg一次,两次都保证了可见性


  • A write to a volatile field happens before every subsequent read of that same volatile.
  • 可以理解为volatile的写操作对后续的读可见,也是lock addl操作保证了写volatile的可见性


  • A call to start() on a thread happens before any actions in the started thread.
  • 可以理解为线程start()写线程开始状态对后续线程的其他动作可见,JVM内部处理了,实际实现肯定也是用了lock/内存屏障来实现的,其实在聊聊JVM(九)理解进入safepoint时如何让Java线程全部阻塞 中我们提到了线程状态的改变,在JVM里面是对一个线程状态变量进行原子的修改,这个状态的改变是原子的,并且可见的,当然就具备了Happens-before的能力


  • All actions in a thread happen before any other thread successfully returns from ajoin() on that thread.
  • 可以理解为一个被join的线程中所有的写操作在它join结束后回到原来的线程时,对原来的线程可见。这个和上面的原理差不多,就是JVM在修改线程状态的时候是一次原子操作,并且保证了可见性(估计是一次CAS),所以连带着修改状态前面的修改也都对后续的操作可见了


其他还有一些Happens-before规则,比如CAS操作,原子变量的修改都有Happens-before的含义,另外Happens-before具备传递性,比如 A happens beofre B, B happens before C, 那么A肯定 happens before C。

为什么具备传递性呢,原因还是在开篇的时候说的,lock/内存屏障不仅仅把当前的地址的数据原子的写到缓存和内存,肯定也把这之前CPU缓存/write buffer的脏数据写回到主内存了,这样就实现了Happens before的传递性。


所以所有用到volatile ,synchronized, CAS的地方都具备Happens before的传递性,显式锁和原子变量底层都是基于CAS来实现的,当然用到它们的时候也具备了Happens before的传递性。


所以下面这个例子就很好理解了,比如 y是volatile变量或者是原子变量/同步器类等等用到CAS的

线程A      线程B 

x = 1        a = y

y = 2        b = x


如果在时间顺序上y=2这个对被同步的变量的写先发生于 a = y 这个对被同步的变量的读,那么可以肯定的说 b = x = 1。

有人问 x = 1会不会被重排到 y =2 之后,答案是不会,因为y是个被同步的变量,防止重排序, x 不会跨越内存屏障排到y=2之后,所以

b = x同样也不会被重排序到 a = y前面,因为 y是被同步的变量,内存屏障同样不会让屏障后面的操作跨越到前面去


所以只要 y =2 写操作发生在 a = y读操作之前,那么最后 x = 1 肯定先于 b=x,所以 b = 1


参考资料:

happens-before俗解


聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则