Java Happens-Before规则及其作用

本文主要列出Happens-Before规则,介绍Happens-Before的作用以及JMM中为什么要有Happens-Before规则的存在。

Happens-Before规则主要有两个作用,一个是解决数据竞争问题,另一个是为开发人员提供足够强的内存可见性。

数据竞争

数据竞争就是指并发条件下的状态属性不同步而引发的读写不一致问题
现假设有两个线程A、B要对内存中的同一个变量进行访问,线程A要对这个变量执行写操作,线程B要对这个变量执行读操作,两个操作是同时进行的,此时若不加以限制,线程B读操作所得到的结果就有两种可能,且结果是不可预测的,这并不是开发人员希望看到的结果,在JMM(Java内存模型)设计之初就考虑到了这个问题,必须人为的指定读/写操作的执行顺序,Happens-Before规则应运而生。

重排序

重排序就是指实际应用中,编译器和处理器为了优化程序性能而对指令序列进行重新排序的手段。即代码或操作在实际执行时有可能出于优化考虑被改变顺序,并不一定按照代码编写时的顺序执行,由此引发内存可见性问题,重排序分为以下三类:

  1. 编译器优化的重排序:在不改变单线程程序语义的前提下,可以重新安排语句执行顺序。
  2. 指令级并行的重排序:如果不存在数据依赖,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序:处理器对缓存读/写缓冲区的重排序。

在JMM中,对于重排序采取了如下策略:

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

Happens-Before规则

定义

Happens-Before规则在JRS-133中定义如下:

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

定义1向开发人员保证如果操作A happens-before 操作B,那么操作A的结果将对操作B可见,且操作A的执行顺序在操作B之前。

定义2对编译器和处理器进行约束,重排序必须在不改变程序执行结果(单线程条件下)的前提下进行。

具体规则

Happens-Before规则共包含以下八条,程序执行时会严格按照规则进行。

  • 程序顺序规则。如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行。
  • 监视器锁规则。在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
  • volatile变量规则。对volatile变量的写入操作必须在对该变量的读操作之前执行。
  • 线程启动规则。在线程上对Thread.Start的调用必须在该线程中执行的任何操作之前执行。
  • 线程结束规则。线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false。
  • 中断规则。当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted).
  • 终结器规则。对象的构造函数必须在启动该对象的终结器之前执行完成。
  • 传递性。如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

值得一提的是Happens-Before规则并不能完全保证并发环境下程序能够按照开发人员预想的正确执行,也许以单线程的视角,所有线程的执行过程都符合Happens-Before规则且执行结果正确,但站在多线程视角,并发环境的指令重排仍有可能导致程序运行结果不符合预期甚至发生错误,在了解Happens-Before规则的基础上,更应该善用同步原语volatile、synchronized和final对程序做正确的同步处理避免问题发生。

你可能感兴趣的:(JMM,java,并发编程,多线程)