0%

Java-happens-before

阅读更多

1 前言

happens-before是JMM最核心的概念

  • 对应Java程序员来说,理解happens-before是理解JMM的关键
  • JSR-133使用happens-before的概念来阐述操作之间的内存可见性
  • 在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系(这里提到的两个操作既可以在一个线程之内,也可以是在不同线程之间)

2 JMM的设计

在设计JMM时,需要考虑两个关键因素

  • 程序员对内存模型的使用。程序员希望内存模型更易于理解、易于编程。程序员希望基于一个强内存模型来编写代码
  • 编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型

上述两个因素相互矛盾,所以JSR-133专家组在设计JMM内存模型时的核心目标就是找到一个好的平衡点:

  1. 一方面,要为程序员提供足够强的内存可见性保证
  2. 另一方面,对编译器和处理器的限制要尽可能放松

JMM把happens-before要求禁止的重排序分为了下面两类

  1. 会改变程序结果的重排序
  2. 不会改变程序结果的重排序

JMM对这两种不同性质的重排序,采取了不同的策略

  1. 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序
  2. 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)

JMM其实遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行

  • 例如,如果编译器经过细致分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除
  • 再如,如果编译器经过细致分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当做一个普通变量来对待
  • 这些优化既不会改变程序的执行结果,又能提高程序的执行效率

什么叫做正确同步?我的理解是:对于一个或者几个共享变量,同步会使得这些共享变量的访问操作串行化,于是在同一时刻只有一个线程能够访问这些变量。如果我们不关心具体的线程,那么正确同步的多线程程序也可以"理解为或等效于"一个单线程程序

3 happens-before 定义

JSR-133使用happens-before的概念来指定两个操作之间的执行顺序,由于这两个操作可以在一个线程之内,也可以在不同线程之间。因此JMM,通过happens-before关系向程序员提供跨线程的内存可见性保证

《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义如下

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
    • 第一个关键点:可见性
    • 第二个关键点:执行顺序(但是第二条规则可能会破坏这种执行顺序)
    • 这条规则是JMM对程序员的承诺,从程序员的角度可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型向程序员保证–A操作的结果将对B可见,且A的执行顺序(可以理解为,但真实是否如此,要看编译器和处理器是否进行优化)排在B之前。注意,这只是JMM向程序员做出的保证
  2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,域按happens-before关系来执行的结果一致,那么这种重排序并不非法(JMM允许这种重排)
    • 这条规则是JMM对编译器和处理器重排序的约束原则。前面提到过,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这样做的原因是:程序员对于这两个操作是否真的被重排并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)

本质上,happens-before关系和as-if-serial语义是一回事

  • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的结果不被改变
  • as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按照程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的

4 happens-before规则

  1. 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作,准确的说,应该是控制流顺序而不是程序代码顺序
  2. 管程锁定规则(Monitor Lock Rule):一个unlock操作线性发生于后面对同一个锁的lock操作
  3. volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面这个变量的读操作,后面指时间上的先后顺序
  4. 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
  5. 线程终止规则(Thread Termination Rule):线程中所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测到线程已经终止执行
  6. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生
  7. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
  8. 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论

5 参考

  • 《Java并发编程的艺术》