程序员社区

JMM内存模型之happens-before

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

  • 会改变程序执行结果的重排序。
  • 不会改变程序执行结果的重排序。
    JMM对这两种不同性质的重排序,采取了不同的策略,如下:
  • 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
  • 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种 重排序)。
    从上面的分析可以看出,JMM其实是在遵 循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序), 编译器和处理器怎么优化都行。例如,如果编译器经过细致的分析后,认定一个锁只会被单个 线程访问,那么这个锁可以被消除。再如,如果编译器经过细致的分析后,认定一个volatile变 量只会被单个线程访问,那么编译器可以把这个volatile变量当作一个普通变量来对待。这些 优化既不会改变程序的执行结果,又能提高程序的执行效率。

1. happens-before的定义

    1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作 可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 2.两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系 来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
    上面的 1 是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关 系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见, 且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!
    上面的 2 是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵 循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),
    编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重 排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因 此,happens-before关系本质上和as-if-serial语义是一回事。
  • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同 步的多线程程序的执行结果不被改变。
  • as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺 序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正 确同步的多线程程序是按happens-before指定的顺序来执行的。
  • as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提 下,尽可能地提高程序执行的并行度。

2. happens-before的规则

1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的 读。
4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的 ThreadB.start()操作happens-before于线程B中的任意操作。
6)join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before于线程A从ThreadB.join()操作成功返回。
这里的规则1)、2)、3)和4)前面都讲到过,这里再做个总结。由于2)和3)情况类似,这里 只以1)、3)和4)为例来说明。下图是volatile写-读建立的happens-before关系图。

JMM内存模型之happens-before插图
happens-before关系示意图

结合上图,我们做以下分析。

  • 1 happens-before 2和3 happens-before 4由程序顺序规则产生。由于编译器和处理器都要 遵守as-if-serial语义,也就是说,as-if-serial语义保证了程序顺序规则。因此,可以把程序顺序 规则看成是对as-if-serial语义的“封装”。
  • 2 happens-before 3是由volatile规则产生。前面提到过,对一个volatile变量的读,总是能看到(任意线程)之前对这个volatile变量最后的写入。因此,volatile的这个特性可以保证实现 volatile规则。
  • 1 happens-before 4是由传递性规则产生的。这里的传递性是由volatile的内存屏障插入策 略和volatile的编译器重排序规则共同来保证的。 下面我们来看start()规则。假设线程A在执行的过程中,通过执行ThreadB.start()来启动线 程B;同时,假设线程A在执行ThreadB.start()之前修改了一些共享变量,线程B在开始执行后会 读这些共享变量。下图是该程序对应的happens-before关系图。

    JMM内存模型之happens-before插图1
    happens-before关系示意图

    在上图中,1 happens-before 2由程序顺序规则产生。2 happens-before 4由start()规则产 生。根据传递性,将有1 happens-before 4。这实意味着,线程A在执行ThreadB.start()之前对共享 变量所做的修改,接下来在线程B开始执行后都将确保对线程B可见。 下面我们来看join()规则。假设线程A在执行的过程中,通过执行ThreadB.join()来等待线 程B终止;同时,假设线程B在终止之前修改了一些共享变量,线程A从ThreadB.join()返回后会 读这些共享变量。下图是该程序对应的happens-before关系图。

    JMM内存模型之happens-before插图2
    happens-before关系示意图

    在上图中,2 happens-before 4由join()规则产生;4 happens-before 5由程序顺序规则产生。 根据传递性规则,将有2 happens-before 5。这意味着,线程A执行操作ThreadB.join()并成功返 回后,线程B中的任意操作都将对线程A可见。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » JMM内存模型之happens-before

一个分享Java & Python知识的社区