Java 并发编程之 happens-before 规则
Posted 琦彦
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 并发编程之 happens-before 规则相关的知识,希望对你有一定的参考价值。
目录
引言
happens-before 字面意思就是先行发生,你可以理解为 A happens before B,就是 A 发生在 B 之前。
happens-before(HB) 是在 JMM 中的一个很重要的规则,即一个操作的结果对于另一个操作是可见的,用来指定两个操作之间的执行顺序。
那为什么要有这个规则呢?
其一是为了解决多线程的共享数据的可见性问题;
其二是为了解决一些指令重排序问题,JMM 对编译器和处理器指令重排序的约束原则。
唯有如此,才能保证我们写的程序按我们预想的方式执行,得到想要的结果。
假设程序有两个操作 A 和 B,B 操作需要 A 操作的结果。这两个操作可以在在同一线程中完成,或者在两个不同的线程中完成,happens-before 能向我们保证 A 操作的结果对 B 操作是可见的。A 和 B 之间存在 happens-before 关系。
JMM
JMM 即 Java 内存模型,它是对共享内存的并发模型。我们知道,Java 中的共享变量是存储在主内存中的,而线程有自己的工作内存,如果一个线程要操作一个共享变量,它会将共享变量赋值一份到自己的工作内存,进行操作后,再将最新的变量值回写到主内存中。
假设有线程 a 对共享变量 x 读取进行更新后及时回写到主内存,然后线程 b 再读取共享变量 x 的值进行操作,这是正常没问题的。但是如果线程 a 对共享变量 x 更新后没有及时回写到主内存,这时线程 b 读取到共享变量 x 的值进行操作,这就出现脏读的现象。
要处理以上并发脏读的问题也简单,可以使用同步机制控制多线程之间操作的顺序,例如使用 synchronzied 关键字;或者使用 volatitle 关键字强制将线程更新后的最新值回写到主内存,以便其他线程可见。这都是 JMM 中 HB 的体现。
指令重排序
我们知道,JVM 会对我们写的代码进行优化,其中一个优化点就是编译器和处理器对指令进行重排序。虽然这些优化能提高程序性能,但是有可能会出现优化后执行的结果不是我们预想的结果,所以需要 happens-before 规则来禁止一些编译优化的场景,保证并发编程的正确性。
JMM 并不是全部禁止指令重排序,对于不会改变程序执行结果的重排序,JMM 允许编译器和处理器这样做。而对于会改变程序执行结果的重排序,JMM 会禁止这种重排序。
以我们最熟悉的 double-check 懒汉式单例模式为例,网上会告知如下程序是没问题的。
package com.chenpi;
/**
* @Description
* @Author Mr.nobody
* @Date 2021/6/22
* @Version 1.0
*/
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
if (null == singleton) {
synchronized (Singleton.class) {
if (null == singleton) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
如果分析底层的话,以下一行代码并不是原子操作。在 JVM 指令执行来看,一般会有如下步骤:
- 分配内存给对象
- 初始化对象,即生成实例
- 将分配的内存地址赋值给对象,此时对象不为null
如果按上面的步骤执行的话,并发情况不会有问题。但是因为由于 JVM 编译优化的影响,有可能导致2和3步骤颠倒位置,即发生指令重排序。如果执行了第3步骤而还没执行第2步骤,在并发情况下,如果有另一个线程判断此时变量不为 null,则返回没有初始化的对象进行使用,就有可能会出现报错。
singleton = new Singleton();
解决上述的问题也简单,用 volatitle 关键字修饰 singleton 变量即可,这也是 happens-before 规则之一。
private static volatile Singleton singleton = null;
为何要有 happens-before
其实 happens-before 更像一个发挥中间层的作用。向我们程序员保证一些操作之间的执行顺序,例如 A happens-before B,就保证 A 一定先行发生于 B。它让我们能以简单易懂的方式去写代码,而不用去学习底层比较复杂的例如内存模型,指令重排序等知识。其二是对编译器和处理器做一些约束,它们可以做任何代码优化,但是得保证优化后不能改变原有程序的执行结果,这里主要指单线程程序和正确同步的多线程程序,不然禁止它们优化。
简而言之,就是 happens-before 向我们保证了在多线程环境中,上一个操作对下一个操作的有序性和操作结果的可见性。
其实 Java JUC 类库中许多操作都使用到了 happens-before 规则,例如并发容器,CountDownLatch,Semaphore,Future,Executor等。
何为理解在 happens-before 规则的情况下,也允许编译器和处理器进行优化呢?例如程序有一个加锁的操作,但是编译器分析发现这个锁只能被单线程访问,那其实就没必要加锁操作了,就可以优化消除这个锁。这样能提高程序的执行效率,还不会改变原有的执行结果。
happens-before 规则
happens-before 是从 JDK5 开始,java 使用新的 JSR -133 内存模型推出的一个规则。JSR-133 使用 happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
• Each action in a thread happens-before every subsequent action in that thread.
• An unlock on a monitor happens-before every subsequent lock on that monitor.
• A write to a volatile field happens-before every subsequent read of that volatile.
• A call to start() on a thread happens-before any actions in the started thread.
• All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
• If an action a happens-before an action b, and b happens before an action c, then a happensbefore c.
• the completion of an object’s constructor happens-before the execution of its finalize method (in the formal sense of happens-before).
翻译过来加上自己的理解就是:
- 程序次序规则:同一个线程内的一段代码的执行顺序是有序的,即前面的操作 happens-before 后面的操作。但还是有可能发生指令重排序,不过重排序后的结果还是跟顺序执行的结果一致。
- 管程锁定规则:对一个锁的解锁操作 happens-before 后续对这个锁的加锁操作。即后续的加锁操作能够感知到前面解锁的变化,synchronized 就是管程的实现。
- volatile 变量规则:对 valatile 修饰的变量的更新操作 happens-before 后续对此变量的任意操作。可以了解下内存屏障和缓存一致性协议。
- 传递性规则:A happens-before B,B happens-before C,则 A happens-before C。学过离散数学的都知道偏序关系,偏序关系具有传递性。
- 线程启动规则:在主线程启动子线程,那么主线程启动子线程之前的操作对于子线程是可见的。即 start() happens-before 子线程中的操作。
- 线程终止规则:在主线程执行过程中,子线程终止,那么子线程终止之前的操作在主线程中是可见。例如在主线程中执行子线程的 join 方法等待子线程完成,当子线程执行完毕后,主线程可以看到子线程的所有操作。
- 线程中断规则:对线程 interrupt 方法的调用 happens-before 被中断线程代码检测到中断事件。
- 对象终结规则:一个对象的构造函数执行的结束 happens-before 它的 finalize()方法。
以上是关于Java 并发编程之 happens-before 规则的主要内容,如果未能解决你的问题,请参考以下文章
阿里面试题:Java 并发编程之 happens-before 规则
Day822.Happens-Before 规则 -Java 并发编程实战
Day822.Happens-Before 规则 -Java 并发编程实战