JVM中happens-before理解与应用.md

happens-before 对于 Java 开发者来说可能并不陌生, 尤其是面对并发编程的时候, 更是个无法避免的话题. 为了学习 Java 并发编程, 找了各个方面的资料, 在总结理解各个方面的资料后, 写出自己的理解与想法.

什么是 happens-before ?

Wikipedia

In computer science, the happened-before relation is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow). This involves ordering events based on the potential causal relationship of pairs of events in a concurrent system, especially asynchronous distributed systems. It was formulated by Leslie Lamport.[1] In Java specifically, a happens-before relationship is a guarantee that memory written to by statement A is visible to statement B, that is, that statement A completes its write before statement B starts its read.Happened-before

java.util.concurrent 包说明

Chapter 17 of The Java™ Language Specification defines the happens-before relation on memory operations such as reads and writes of shared variables. The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.Memory Consistency Properties

<深入理解 Java 虚拟机>

先行发生是 Java 内存模型中定义的两项操作之间的偏序关系, 如果说操作 A 先行发生于操作 B, 其实就是说发生操作 B 之前, 操作 A 产生的影响能被操作 B 观察到, “影响” 包括修改了内存中共享变量的值, 发送了消息, 调用了方法等.[<深入理解 Java 虚拟机>]

理解与总结

通过上述三个定义可以肯定, happens-before 是定义在两个操作之间的一种关系, 用来断言操作的结果或者影响能不能被另一个操作观察到. 最直观是在并发编程中, 线程 t1 的操作 a 是否能被线程 t2 的操作 b 观察到, 即 b 操作能不能正确的接受到 a 的操作结果.(“观察到” 这个操作是指, 当 t1 线程中的 a 操作写入了一个数据, 在 t2 线程中能否保证正确的被 t2 线程的 b 操作读取到.)

不过在我的学习过程中, 理解这句话并不是一帆风顺的, 具体问题出现在: 既然 t1 的操作能被 t2 观察到, 那么为什么增加了 happens-before 的操作中, 依旧会出现结果不正确的情况呢(参考code 2)?

example

  • env: Windows 10, JDK 1.8, IDEA

volatile 可见性

code 1:

public class Main {

    static int i;

    public static void main(String[] args) {
        Thread th1 = new Thread(() -> i = 1);
        Thread th2 = new Thread(() -> {
            int j = i;
        });

        th1.start();
        th2.start();
    }
}

上述 Java 代码在执行的时候, 会有两种代码轨迹:

  • 1 th1:(i = 1) -> th2: (int j = i) 最终: i = 1, j = 1 或 i = 1, j = 0
  • 2 th2:(int j = i) -> th1:(i = 1) 最终: i = 1, j = 0

在第一种情况下 j 也有可能为 0 是由于 Java 内存模型对普通变量的读写并不保证可见性, 所以即使 th1 在修改了 i 的情况下, 工作内存还未写回主内存, th2 操作就已经完成, 所以即使 th1 先执行, 但依旧不能保证操作结果对外可见, 也就是说其他线程可能观察不到 th1 的修改, 导致了错误的数据.

要保证 Java 第一种情况不出现 j = 0 的情况, 则需要使用 happens-before 来保证 th1 的操作结果对 th2 可见, 此时我们可以采取以下的方式:

  • 对 i 增加 volatile 修饰
  • 对 th1 th2 增加 synchronized 修饰
    为了简单, 这里使用第一种方法:

code 2:

public class Main {

        static volatile int i;

        public static void main(String[] args) {
                Thread th1 = new Thread(() -> i = 1);
                Thread th2 = new Thread(() -> {
                                int j = i;
                                });

                th1.start();
                th2.start();
        }
}

volatile 保证了操作的可见性, 则第一种执行轨迹, 结果中 j 不再可能为 0 因为在这种执行轨迹下,i 的任何写入操作对 j 都是可见的.但是第二种执行轨迹, j 依旧会是 0 .

happens-before

以下是来自 <深入理解 Java 虚拟机> 中对 happens-before 的介绍:

  • 程序次序规则: 在一个线程内, 按照程序代码顺序, 书写在前面的操作先行发生于书写在后面的操作. 准确的说, 应该是控制流顺序而不是程序代码顺序, 因为要考虑分支、循环等结构.
  • 管程锁定规则: 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作. 这里必须强调的是同一个锁, 而”后面”是指时间上的先后顺序.
  • volatile 变量规则: 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作, 这里的”后面”同样是指时间上的先后顺序.
  • 线程启动规则: Thread 对象的 start() 方法先行发生于此线程的每一个动作.
  • 线程终止规则: 线程中的所有操作都先行发生于对线程的终止检测, 我们可以通过 Thread.join() 方法结束, Thread.isAlive() 的返回值等手段检测到线程已经终止执行.
  • 线程中断规则: 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生, 可以通过 Thread.interrupted() 方法检测到是否有中断发生.
  • 对象终结规则: 一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始.
  • 传递性: 如果操作 A 先行发生于操作 B, 操作 B 先行发生于操作 C, 那就可以得出操作 A 先行发生于操作 C 的结论.
    以上是书中介绍的 Java 不需要通过同步手段就能保证的 happens-before 规则.

    应用

    Java Memory Model 围绕着并发操作中的原子性、可见性、有序性特征来建立, 基于 JMM 的 JVM 以及其上的 Java 语言上的并发编程, 则通过 happens-before 可以保证代码正确处理了并发中的这三个特征. 可以说, happens-before 是并发编程的利器.
过着四分幻想六分生活 O(∩_∩)O