[Java 并发编程] 11. Java Happen Before Guarantee

文章目录

  • 前言
  • 一、指令重排
  • 二、指令重排在多CPU计算机中的问题
  • 三、volatile 可见性保证
    • 3.1 volatile 修改数据可见性保证
    • 3.2 volatile 读取数据可见性保证
    • 3.3 volatile happens-before 保证
  • 四、synchronized 可见性保证
    • 4.1 synchronized 锁进入可见性保证
    • 4.2 synchronized 锁退出可见性保证
    • 4.3 synchronized happens-before guarantee


前言

Java Happen Before Guarantee 是JVM(Java虚拟机)与CPU为了提高性能允许指令重排的一组管理规则。Happen Before Guarantee 主要包含访问 volatile 变量或访问 synchronized 代码块中的变量。


一、指令重排

现代计算机有能力并行执行指令,当一个指令不依赖其他指令时,可能发生指令重排。如下所示:两个指令不相互依赖,计算机可以并行执行这两个指令。

a = b + c;
d = e + f;

下面这2个指令不会发生指令重排,因为第二个指令依赖第一个指令产生的结果。

a = b + c;
d = a + e;

指令重排的结果可以让指令在CPU中并行执行,以提高性能。指令重排在JVM和CPU中是被允许的,前提是程序中的语句没有发生改变。指令重排后程序执行的结果必须与没有指令重排时程序执行的结果保持一致。


二、指令重排在多CPU计算机中的问题

指令重排在多线程、多CPU系统中存在一些挑战。请看下面示例:

private static int a = 0;
private static int b = 0;

new Thread(() -> {
    a = 1;
    b = 1;
}).start();

new Thread(() -> {
    if (a == 0 && b == 1) {
        System.out.println("有点意思");
    }
}).start();

在第一个线程中,a = 1 与 b = 1 两个指令不相互影响,CPU为了提高执行性能,可能并行执行这两个指令,这种情况下, b = 1 指令可能在 a = 1 前面执行,若执行 b = 1 后(假定 a = 1 指令还未执行,此时 a 的值为初始值 0),此时第二个线程正在执行判断条件 a == 0 && b == 1,那么将会打印数据 ‘有点意思’。为了验证这个问题,我们来循环执行这段代码。如下:

/**
 * 测试指令重排
 *
 * @author : sungm
 * @date : 2020-08-13 15:14
 */
public class Main {

    private static int a = 0;
    private static int b = 0;

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            Thread t1 = new Thread(() -> {
                a = 1;
                b = 1;
            });
            Thread t2 = new Thread(() -> {
                if (a == 0 && b == 1) {
                    System.out.println("有点意思");
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            a = 0;
            b = 0;
        }
    }
}

如果你觉得这段代码有意思,不妨自己试一试。多等待一会,你会发现控制台输出了“有点意思”。


三、volatile 可见性保证

Java volatile 关键字提供了读写的可见性保证,当线程读volatile变量时会从主内存中读取数据,当线程修改volatile变量时会将变量的值写回到主内存中。这种同步到主内存的机制保证了变量的值对其他线程可见,这就是volatile可见性保证。

3.1 volatile 修改数据可见性保证

当线程修改volatile变量的值时,修改后的值会被同步到主内存中。另外,线程里包含的所有变量都会随volatile变量写回到主内存中。(请注意这句话,线程所有的变量都会随volatile变量写回到主内存中,不只是volatile变量写回到主内存中)

请看示例:
(1) 首先我们用简单的代码证明使用 volatile 定义的变量的值被某个线程修改后对其他线程可见。:

class MyRunnable implements Runnable {

    //注意这里没有使用volatile关键字
    private boolean keepRunning = false;

    public boolean isKeepRunning() {
        return keepRunning;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //子线程把keepRunning的值改为true
        keepRunning = true;
    }
}

//主方法
public static void main(String[] args) {
    MyRunnable myRunnable = new MyRunnable();

    //启动子线程
    new Thread(myRunnable).start();

    //主线程中循环尝试获取子线程修改后的keepRunning的值,如果获取到,输出有点意思
    while (true) {
        if (myRunnable.isKeepRunning()) {
            System.out.println("有点意思");
        }
    }
}

如果你亲手运行了这段代码,你会发现这个程序永远不会输出“有点意思”。现在我们使用volatile定义keepRunning属性,其他代码不变。

//使用volatile定义
private volatile boolean keepRunning = false;

使用volatile关键字后,程序循环输出“有点意思”。

因此我们可以得出结论:volatile 定义的变量的值被某个线程修改后对其他线程可见。

(2) 现在我们来证明下我们前面说的:线程里包含的所有变量都会随 volatile 变量写回到主内存中。

class MyRunnable implements Runnable {

    //使用了 volatile 关键字的属性
    private volatile boolean keepRunning = false;

    //未使用 volatile 关键字的属性
    private String strA = "A";
    private String strB = "B";

    @Override
    public void run() {
        try {
            Thread.sleep(100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        keepRunning = true;
        strA = "a";
        strB = "b";
    }

    @Override
    public String toString() {
        return "MyRunnable{" +
                "keepRunning=" + keepRunning +
                ", strA='" + strA + '\'' +
                ", strB='" + strB + '\'' +
                '}';
    }
}

//主方法
public static void main(String[] args) {
    MyRunnable myRunnable = new MyRunnable();

    //启动子线程
    new Thread(myRunnable).start();

    try {
        Thread.sleep(200L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println(myRunnable.toString());
}

程序最终输出:MyRunnable{keepRunning=true, strA=‘a’, strB=‘b’}

这个结果我们不难看出:子线程修改了keepRunning、strA和strB的值(strA和strB未使用volatile关键字),主线程不仅读取到了keepRunning更新后的值,还读取到了strA和strB更新后的值。所以请记住:线程里包含的所有变量都会随volatile变量写回到主内存中。

3.2 volatile 读取数据可见性保证

当线程读取被 volatile 关键字修饰的变量时,会从主内存中读取。另外:线程里所有的变量都会随着 volatile 变量读取到CPU缓存或者寄存器中。

比如下面三个变量,当线程读取strA的值时,会重定向到主内存中读取strA的值,如果strB,strC同样在线程中,也会从主内存中读取strB,strC的值。我们本章3.1节的例子同样能证明此特性。

valatile String strA = "A";
String strB = "B";
String strC = "C";

3.3 volatile happens-before 保证

volatile happens-before 保证设置了一些关于volatile变量指令重排的限制(规定)。

volatile write happens-before guarantee

volatile write happens-before guarantee: 所有volatile写之前的指令不允许被重排序到volatile指令后面。

示例:

nonVolatileVariableA = "a";
nonVolatileVariableB = "b";
volatileVariableC = "c";

示例中 nonVolatileVariableA、nonVolatileVariableB两个变量是没有被volatile修饰的变量,volatileVariableC变量被volatile修饰。volatile write happens-before 保证了前面两个指令不能重排序到第三个指令后面,也就是不会发生下面这类重排序:

volatileVariableC = "c";
nonVolatileVariableA = "a";
nonVolatileVariableB = "b";

但是示例可能会发生下面这种重排序:

nonVolatileVariableB = "b";
nonVolatileVariableA = "a";
volatileVariableC = "c";

volatile read happens-before guarantee

volatile read happens-before guarantee: 所有volatile读之后的指令不允许被重排序到volatile指令前面。

示例:

volatileVariableC;
nonVolatileVariableA;
nonVolatileVariableB;

volatile read happens-before 保证了最后两个指令不能重排序到第一个指令前面,也就是不会发生下面这类重排序:

nonVolatileVariableA;
nonVolatileVariableB;
volatileVariableC;

示例可能会发生下面这种重排序的结果:

volatileVariableC;
nonVolatileVariableB;
nonVolatileVariableA;

总结volatile happens-before guarantee:所有volatile写之前的指令不允许被重排序到volatile指令后面;所有volatile读之后的指令不允许被重排序到volatile指令前面。(简记:volatile写之前读之后)


四、synchronized 可见性保证

synchronized 可见性保证 与 volatile 可见性保证非常相似。

4.1 synchronized 锁进入可见性保证

当一个进程进入 synchronized同步代码块(或同步方法),线程内所有可见变量都将从主内存中读取数据。

4.2 synchronized 锁退出可见性保证

当一个进程退出 synchronized同步代码块(或同步方法),线程内所有可见变量数据都将写回到主内存中。

示例:

class Demo {

    private int numberA = 1;
    private int numberB = 2;
    private int numberC = 3;

    //省略getter/setter方法

    void copyNumber(Demo demo) {
        synchronized (this) {
            this.numberC = demo.getNumberC();
        }
        this.numberA = demo.getNumberA();
        this.numberB = demo.getNumberB();
    }

}

说明:当某个线程进入 copyNumber() 方法的 synchronized 代码块时,线程内所有的可见变量都会从主内存中加载数据,也就是说 this 对象的 numberA, numberB也会从主内存中读取数据;退出synchronized 代码块时,线程内所有的可见变量都会写回到主内存中,this 对象的 numberA, numberB修改后的值也会被写回到主内存中。

4.3 synchronized happens-before guarantee

synchronized 提供了两种 happens-before guarantee :一种与开始进入synchronized 代码块有关;另外一种与退出synchronized 代码块有关。

synchronized beginning happens-before guarantee :

我们已经知道,当线程进入 synchronized 代码块时,线程所有可见变量都将从主内存中读取数据。

请看示例:

void getNumber(Demo demo) {
    synchronized (this) {
        this.numberC = demo.getNumberC();
    }
    this.numberA = demo.getNumberA();
    this.numberB = demo.getNumberB();
}

当线程进入 synchronized 代码块时,线程所有可见变量 this.numberA, this.numberB, this.numberC 都将从主内存中读取数据。

对于上面这个示例,所有的变量的读取指令都不会重排序到进入 synchronized 代码块指令前面。 也就是说,不会发生下面这种情况:

void getNumber(Number n) {
    this.numberA = n.getNumberA();
    this.numberB = n.getNumberB();
    synchronized (this) {
        this.numberC = n.getNumberC();
    }
}

synchronized end happens-before guarantee :

我们已经知道,当线程退出 synchronized 代码块时,线程所有可见变量的数据都将写回到主内存中。

请看示例:

void getNumber(Demo demo) {
    synchronized (this) {
        this.numberC = demo.getNumberC();
    }

    this.numberA = demo.getNumberA();
    this.numberB = demo.getNumberB();
}

当线程退出 synchronized 代码块时,线程所有可见变量 this.numberA, this.numberB, this.numberC 的数据都将写回到主内存中。

对于上面这个示例,所有的变量的写的指令都不会重排序到退出 synchronized 代码块指令前面。 也就是说,不会发生下面这种情况:

void copyNumber(Number n) {
    synchronized (this) {
        this.numberC = n.getNumberC();
    }
    this.numberA = n.getNumberA();
    this.numberB = n.getNumberB();
}

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