美文网首页一些收藏
Java小白系列(七):Java Happens-Before规

Java小白系列(七):Java Happens-Before规

作者: 青叶小小 | 来源:发表于2021-02-09 01:30 被阅读0次

一、前言

Java Happens-Before是一组规则,来管理JVM和CPU如何通过指令重排来提升性能。通过调整多线程间变量相互依赖性,且不影响最终的结果,来达到提升并发性能。如果没有这个规则,那么指令的重排很可能导致最终的结果将是错的。

二、指令重排

现代CPU是多CPU或多核架构,我们在《小白六》提到过,因此,当指令相互没有依赖时,多CPU的架构可以并行执行;如果指令存在相互依赖,并行执行将可能出错。

  • 指令没有相互依赖,可以并行执行
a = b + c
x = y + z
  • 指令有相互依赖,不能并行执行
a = b + c
x = a + y

可以看到,第二条指令依赖第一条指令的计算结果,如果两条指令并行执行,x 的值将不正确。

  • 指令部分依赖
a = b + c  // 1
x = a + y  // 2
i = j + k  // 3
o = p + q  // 4

上面这种情况,我们可以通过指令重排来并发执行,提升性能:

a = b + c  // 1
i = j + k  // 3
o = p + q  // 4
x = a + y  // 2

我们看到,指令重排只要不改变程序的语义(结果),那么是被允许的。

三、多CPU指令重排的问题

我们用生产一帧 / 绘制一帧 来举例:

public class ReOrderProblem {
    private boolean hasNewFrame = false;
    private Object frame = null;

    private long taken = 0;
    private long gen = 0;

    public void genFrame(Object frame) {
        this.frame = frame;
        gen ++;
        hasNewFrame = true;
    }

    public Object takeFrame() {
        while (!hasNewFrame) {
            // 等待...直到有新的frame
        }

        Object o = frame;
        taken ++;
        hasNewFrame = false;
        return o;
    }
}

在『genFrame』方法中,三条指令没有任何依赖,因为,JVM / CPU将可能通过指令重排来提升执行速度,如下:

    public void genFrame(Object frame) {
        gen ++;
        hasNewFrame = true;
        this.frame = frame;
    }

在 JVM / CPU 角度看来,对象的引用传递可能相比其它两条指令要慢,因此,通过如上调整,可能性能会提升,但如果这么做了,就会造成『takeFrame』出问题(并不会引起 crash,但却可能绘制上一帧,因此浪费了一次 CPU)。

四、Happens-Before 规则

该规则的提出,就是用来解决,只要不改变程序的结果,那么指令重排是OK的,我们之前有稍微讲到过该规则,这里,我们将对其中几条具体分析。

4.1、(同一个线程中)程序顺序规则

在同一个线程中,按照程序的顺序,前面的优先于后面的操作执行。
我们也说过,再保证结果不变的情况下,可以进行指令重排,例如我们上面『指令没有相互依赖』或『指令部分依赖』时。而当有依赖时,则不允许指令重排。

a = b + c
x = a + y

此时,只能顺序执行,不允许指令重排。

4.2、监视器锁规则

我们讲过 synchronized ,因此知道,监视器锁是 Java 内置的锁,用来线程同步的。

synchronized(object) {
    System.out.println("x = " + x);
    x = 1;
}

两个线程,其中一个线程在同步块中,另一个因为锁而被阻塞;当在同步块中的线程退出时,另一个被阻塞的线程进入同步块时,x的值为上一个线程修改后的值。

4.3、volatile 写对后续的读可见

这个就是我们之前讲到过的,当一个 volatile 变量被修改时,JMM会让其它线程的工作内存该变量副本失效,后续操作需要从主内存中获取最新的值。
除了缓存失效,还有一点,我们需要『4.4 传递性』来一起看(涉及到volatile变量与普通变量的指令重排序问题)。

4.4、传递性

如果A操作先于B操作,B操作先于C操作,那么A操作先于C操作。这个可以理解。
好了,我们来看看『4.3』中关于 volatile 变量与普通变量的指令重排序:

class Example {
    private int x = 10;
    private volatile boolean v = false;
    
    public void writer() {
        x = 2;
        v = true;
    }
    public void reader() {
        if (v == true) {
            // 这里x会是多少呢?
        }
    }
}

如下图分析

传递性.png

我们可以通过上图依次分析:

  • 『x = 2』 Happens-Before 『v = true』(规则 1);
  • 『v = true』Happens-Before 『读 v』(规则 3);
  • 根据上面两条,再结果『规则 4』,我们可以得出结论:『读 x = 2』

这点是 JDK1.5 中 JSR-133 对 volatile 的增强!

4.5、线程 start 规则

这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

4.6、线程 join 规则

如果线程A执行线程B.join,并成功返回,那么,线程B的所有操作都优先于 join 返回操作。

五、总结

我们上一篇讲了 JMM,JMM分两部分,一部分就是 Happens-Before 规则,另一部分就是我们上一篇谈到的在JVM中的实现原理。只有掌握了JMM,我们才能更好的设计出健壮且正确的程序。

相关文章

网友评论

    本文标题:Java小白系列(七):Java Happens-Before规

    本文链接:https://www.haomeiwen.com/subject/garqxltx.html