Happens-Before 原则深入解读
Happens-Before(先行发生)原则是对 Java 内存模型(JMM)中所规定的可见性的更高级的语言层面的描述。用这个原则解决并发环境下两个操作之间的可见性问题,而不需要陷入 Java 内存模型苦涩难懂的定义中。关于 Java 内存模型中所规定的可见性定义本文不再叙述,感兴趣的读者可参考的书籍有《深入理解 Java 虚拟机》和《Java 并发编程的艺术》。
1 Happens-Before(先行发生)原则的定义
程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
线程启动规则(Thread Start Rule):Thread 对象 start()方法先行发生于此线程的每一个动作。
线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread.join()方法和 Thread.isAlive()的返回值等手段检测线程是否已经终止执行。
线程中断规则(Thread Interruption Rule):对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted()方法检测到是否有中断发生。
对象终结规则(Finalizer Rule) :一个对象的初始化完成(构造函数结束)先行发生于它的 finalize()方法的开始。
传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。
Happens-Before 原则最难以理解的地方在于如何理解"Happens-Before(先行发生)"这个词。我们以程序次序规则为例,“书写在前面的操作先行发生于书写在后的操作”,如果理解为“书写在前面的操作比书写在后面的操作先执行”看起来是没有什么问题的,写在前面的操作确实在程序逻辑上比写在后面的操作先执行。按照同样的理解,我们看一下管程锁定规则,“unlock 操作先行发生于后面对同一个锁的 lock 操作”,如果理解为“unlock 操作比同一个锁的 lock 操作先执行”这就很困惑了,还没有加锁,怎么解锁。
之所出现这种困惑的解读方式,是因为把“先行发生”理解为一种主动的规则要求了,而“先行发生”事实上是程序运行时出现的客观结果。正确的解读方式是这样的,对于“同一把锁”,如果在程序运行过程中“一个 unlock 操作先行发生于同一把锁的一个 lock 操作”,那么“该 unlock 操作所产生的影响(修改共享变量的值、发送了消息、调用了方法)对于该 lock 操作是可见的”。
按照这种理解,依次重新解读其他规则。
程序次序规则:在一个线程内,按照控制流顺序,如果操作 A 先行发生于操作 B,那么操作 A 所产生的影响对于操作 B 是可见的。
管程锁定规则:对于同一个锁,如果一个 unlock 操作先行发生于一个 lock 操作,那么该 unlock 操作所产生的影响对于该 lock 操作是可见的。
volatile 变量规则:对于同一个 volatile 变量,如果对于这个变量的写操作先行发生于这个变量的读操作,那么对于这个变量的写操作所产的影响对于这个变量的读操作是可见的。
线程启动规则:对于同一个 Thread 对象,该 Thread 对象的 start()方法先行发生于此线程的每一个动作,也就是说对线程 start()方法调用所产生的影响对于该该线程的每一个动作都是可见的。
线程终止规则:对于一个线程,线程中发生的所有操作先行发生于对此线程的终止检测,也就是说线程中的所有操作所产生的影响对于调用线程 Thread.join()方法或者 Thread.isAlive()方法都是可见的。
线程中断规则:对于同一个线程,对线程 interrupt()方法的调用先行发生于该线程检测到中断事件的发生,也就是说线程 interrupt()方法调用所产生的影响对于该线程检测到中断事件是可见的。
对象终结规则:对于同一个对象,它的构造方法执行结束先行发生于它的 finalize()方法的开始,也就是说一个对象的构造方法结束所产生的影响,对于它的 finalize()方法开始执行是可见的。
传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,则操作 A 先行发生于操作 C,也就说操作 A 所产生的所有影响对于操作 C 是可见的。
2 示例代码
2.1 管程锁定规则
2.1.1 WithoutMonitorLockRule
如下的这段代码开启了两个线程,updater 和 getter,updater 线程将 stop 变量设置为 true,getter 线程死循环判断 stop 变量为 true 时结束循环。运行这段程序,得到以下输出。
updater set stop true.
getter 线程未输出getter stopped
,说明 updater 线程对 stop 变量的修改对 getter 线程不可见。
2.1.2 MonitorLockRuleSynchronized
如下的这段代码定义了变量 lockObject 作为同步锁,运行这段程序,得到以下输出。
updater set stop true.
getter stopped.
该输出表明 updater 线程对 stop 变量的修改对 getter 线程是可见的。结合 Happens-Before 原则进行分析,根据程序次序规则在 updater 线程内stop = true
先行发生于 lockObject 锁的释放,在 getter 线程内 lockObject 锁的获取先行发生于if (stop)
;再根据传递性则stop = true
先行发生于if (stop)
,所以stop = true
对于if (stop)
是可见的。
2.1.3 MonitorLockRuleReentrantLock
ReentrantLock 也可以起到和 Synchronized 关键字同样的效果,在 Lock 接口的注释中有如下描述。这段描述的意思是说所有的 Lock 接口实现必须在内存可见性上具有和内置监视器锁(Synchronized)相同的语义。
使用 ReentrantLock 编写这段代码如下,同样可以实现和 Synchronized 相同的效果,具体原理在接下来的 volatile 变量规则中讨论。
2.2 volatile 变量规则
2.2.1 WithoutVolatileRule
如下的这段代码开启了两个线程,updater 和 getter,updater 线程将 stop 变量设置为 true,getter 线程死循环判断 stop 变量为 true 时结束循环。运行这段程序,得到以下输出。
updater set stop true.
getter 线程未输出getter stopped
,说明 updater 线程对 stop 变量的修改对 getter 线程不可见。
2.2.2 VolatileRule
使用 volatile 关键字修饰 stop 变量之后运行这段程序得到以下输出。
updater set stop true.
getter stopped.
说明 updater 线程对 stop 的修改对于 getter 线程可见。使用 Happens-Before 原则进行分析,根据 volatile 变量规则,updater 线程对 stop 变量的写操作先行发生于 getter 线程对 stop 变量的读操作,所以 updater 线程将 stop 变量设置为 true 对 getter 线程读取 stop 变量是可见的。
2.2.3 VolatileRule1
volatile 变量还有一个说是特点,其实也不是的特性。如下代码并未将 stop 变量用 volatile 修饰,而是用 volatile 修饰了 volatileObject 变量。运行这段代码将得到如下输出。
updater set stop true.
getter stopped.
上述结果表明虽然 stop 变量未被 volatile 修饰,但是它仍然在 updater 线程和 getter 线程之间可见,在未仔细品读 Happens-Before 原则之前,仅仅从 java 语法上来看是很神奇的。
结合 Happens-Before 原则的程序次序规则和传递性进行仔细分析。updater 线程在将 stop 变量设置为 true 之后,又对 volatileObject 变量进行了赋值,而 getter 线程在读取 stop 变量之前首先读取了 volatileObject。根据程序次序规则在 updater 线程内stop = true
先行发生于volatileObject = new Object()
,在 getter 线程内Object volatileObject = VolatileRule1.volatileObject
先行发生于if (stop)
;再根据传递性则stop = true
先行发生于if (stop)
,所以stop = true
对于if (stop)
就是可见的。
如此分析之后便发现这个特性并不神奇,仅仅是传递性在起作用罢了,对于其他 Happens-Before 原则这个特性同样存在,读者可自行验证。利用此特性的一个典型例子在 jdk 的java.util.concurrent.FutureTask
中,如下的代码节选自FutureTask
,在 outcome 后面的注释上写着non-volatile, protected by state reads/writes
,而 state 是被 volatile 所修饰的。这就是为什么在使用线程池的 submit 方法向线程池提交任务时,执行结果是可见的。而使用 execute 方法向线程池提交任务,这个任务所做的修改却不可见。读者可以自行验证。
上文提到的 ReentrantLock 对管程锁定规则的保证同样和 volatile 变量有关,在java.util.concurrent.locks.AbstractQueuedSynchronizer
类中有state
属性,该属性被 volatile 关键字修饰,不再赘述。
2.3 Thread Start Rule
2.3.1 WithoutThreadStartRule
如下的代码首先开启了 updater 线程,updater 线程调用getter.start
开启了 getter 线程,随后 updater 线程将 stop 设置为 true,getter 线程死循环判断 stop 变量为 true 时结束循环。运行该程序得到如下输出。
updater set stop true.
getter 线程未输出getter stopped
,说明 updater 线程对 stop 变量的修改对 getter 线程不可见。
2.3.2
接下来对调一下 updater 线程中getter.start()
与stop = true
的位置,如下代码所示。运行该程序得到如下输出。
updater set stop true.
getter stopped.
该输出表明 updater 线程对 stop 变量的修改对 getter 线程是可见的。结合 Happens-Before 原则来分析一下,根据程序次序规则在 updater 线程内stop = true
先行发生于getter.start()
;而根据线程启动规则getter.start()
先行发生于该线程内的每一个动作(包括if (stop)
);再根据传递性规则stop = true
先行发生于if (stop)
,所以stop = true
对if (stop)
是可见的。
2.4 线程终止规则
2.4.1 WithoutThreadTermination
如下的这段代码开启了两个线程,updater 和 getter,updater 线程将 stop 变量设置为 true,getter 死循环判断 stop 变量为 true 时结束循环。运行该程序得到以下输出。
updater set stop true.
getter 线程未输出getter stopped
,说明 updater 线程对 stop 变量的修改对 getter 线程不可见。
2.4.2 ThreadTerminationRule
如下的代码 getter 线程在获取 stop 变量之前调用了updater.join()
等待 updater 线程结束。运行这段代码得到以下输出。
updater set stop true.
getter stopped.
该输出结果表明 updater 线程对 stop 变量的修改对 getter 线程是可见的。结合 Happens-Before 原则进行分析,根据线程终止规则 updater 线程中的所有操作(包括stop = true
)先行发生于 getter 线程调用updater.join()
等待 updater 结束;根据程序次序规则在 getter 线程内updater.join()
先行发生于if (stop)
;再根据传递性得出stop = true
先行发生于if (stop)
,所以stop = true
对if (stop)
是可见的。
2.5 线程中断规则
2.5.1 WithoutThreadInterruptRule
如下的这段代码开启了两个线程,updater 和 getter,updater 线程将 stop 变量设置为 true,getter 死循环判断 stop 变量为 true 时结束循环。运行这段代码得到以下输出。
updater set stop true.
getter 线程未输出getter stopped
,说明 updater 线程对 stop 变量的修改对 getter 线程不可见。
2.5.2 ThreadInterruptRule
如下的这段代码 updater 线程在将 stop 设置为 true 之后,调用了getter.interrupt()
方法,而 getter 线程在获取 stop 变量的值之前首先判断了Thread.currentThread().isInterrupted()
,运行这段代码得到以下输出。
updater set stop true.
getter stopped.
该输出结果表明 updater 线程对 stop 变量的修改对 getter 线程是可见的。根据 Happens-Before 原则进行分析,根据程序次序规则在 updater 线程内stop = true
先行发生于getter.interrupt()
,在 getter 线程内Thread.currentThread().isInterrupted()
先行发生于if (stop)
;根据线程中断规则getter.interrupt()
先行发生于Thread.currentThread().isInterrupted()
;再根据传递性得出stop = true
先行发生于if (stop)
,所以stop = true
对于if (stop)
是可见的。
2.6 对象终结规则
2.6.1 FinalizerRule
如下的代码中新建了一个 Test 对象,在 Test 对象的构造方法中将 stop 变量设置为 true,随后将 test 变量赋值为 null,则新建的 Test 对象成为垃圾,再在死循环中分配对象促使垃圾回收。运行该程序得到以下输出。
set stop true in constructor
stop true in finalize, threadName Finalizer
根据程序次序规则在 main 线程中stop = true
先行发生于 Test 的构造方法结束;根据对象终结规则 Test 的构造方法结束先行发生于 Test 的finalize()
方法的开始;再根据传递性得出stop = true
先行发生于finalize()
方法的开始,所以stop = true
对于finalize()
方法是可见的。
对象终结规则的反例特别难以复现,而对象终结规则在编程过程中又很难接触到,所以此处不再举例。
3 进一步解读
在第 2 节中很多例子的可见性保证都结合了程序次序规则和传递性,据此对前几条规则进行进一步解读如下。
管程锁定规则:对于同一个锁,如果一个 unlock 操作先行发生于一个 lock 操作,那么该 unlock 操作(包括 unlock 操作之前的操作)所产生的影响对于该 lock 操作(包括 lock 操作之后的操作)是可见的。
volatile 变量规则:对于同一个 volatile 变量,如果对于这个变量的写操作先行发生于对于这个变量的读操作,那么对于这个变量的写操作(包括写操作之前的操作)所产生的影响对于这个变量的读操作(包括读操作之后的操作)是可见的。
线程启动规则:对于同一个 Thread 对象,该 Thread 对象的 start()方法先行发生于此线程的每一个动作,也就是说对线程 start()方法调用(包括 start 方法之前的操作)所产生的影响对于该该线程的每一个动作都是可见的。
线程终止规则:对于一个线程,线程中发生的所有操作先行发生于对此线程的终止检测,也就是说线程中的所有操作所产生的影响对于调用线程的 Thread.join()方法或者 Thread.isAlive()方法(包括调用这两个方法之后的操作)都是可见的。
线程中断规则:对于同一个线程,对线程 interrupt()方法的调用先行发生于该线程检测到中断事件的发生,也就是说线程 interrupt()方法调用(包括 interrupt 方法调用之前的操作)所产生的影响对于该线程检测到中断事件(包括检测到中断事件之后的操作)是可见的。
对象终结规则:对于同一个对象,它的构造方法执行结束先行发生于它的 finalize()方法的开始,也就是说一个对象的构造方法结束(包括构造方法结束前的操作)所产生的影响,对于它的 finalize()方法开始执行(包括开始之后的操作)是可见的。
对于该进一步解读,以管程锁定规则为例,如下代码并未将stop = true
及if (stop)
包括在 syncronized 代码块内,但是在 updater 线程内stop = true
在 sychronized 代码块之前,而在 getter 线程内if (stop)
在 syncronized 代码块之后。运行该程序得到以下输出。
updater set stop true.
getter stopped.
说明 updater 线程对 stop 的修改对 getter 线程是可见的。
4 总结
Happens-Before 原则在 Java 并发编程中的重要性不言而喻,不理解 Happens-Before 难以写出线程安全又高效的多线程代码。相比于 Java 内存模型 Happens-Before 原则在可见性的描述上要简单得多,但是仍然很拗口并难以琢磨。本文通过通俗化的语言并结合众多实例代码向读者展示了 Happens-Before 原则,希望对各位读者在理解 Happens-Before 原则时能有所帮助。
有任何问题均在可在公众号对话框中回复进一步交流。
关于作者
王建新,转转架构部服务治理负责人,主要负责服务治理、RPC 框架、分布式调用跟踪、监控系统等。爱技术、爱学习,欢迎联系交流。
转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。
关注公众号「转转技术」(综合性)、「大转转 FE」(专注于 FE)、「转转 QA」(专注于 QA),更多干货实践,欢迎交流分享~
评论