【Java】一文读懂happens-before原则

通常,当我们写代码时,我们会假设代码是按照编写的顺序执行的。但事实并非如此,因为出于优化目的,语句的重新排序可能会在编译时或运行时发生。

无论线程何时运行程序,结果都应该像是所有操作都按照它们在程序中出现的顺序发生一样。单线程程序的执行应遵循“as-if-serial”语义。只要保证结果与程序按顺序执行时的结果相同,优化和重新排序是可以被引入的。

让我们看一个例子。

这个代码块:

var i = 0;
var j = 1;
j--;

可以被重新排序为:

var j = 1;
j--;
var i = 0;

我们可以根据前面代码块的结果添加额外的分配:

var x = i+j;

无论发生了怎样的重新排序,结果都应该像是程序的每个语句按顺序执行一样。

从单线程的角度来看,这是没问题的;然而,当多个线程在这样的代码块上操作时,就会出现各种问题。一个线程的操作效果不会以可预测的方式对其他线程可见。

想象一下,一个线程的代码块执行依赖于另一个线程的执行结果。这就是所谓的“happens-before”关系。我们有两个事件,无论重新排序如何,结果都应该像是一个事件发生在另一个事件之前。

Java 提供了 happens-before 保证。

规则

我们可以查看文档了解这些规则。

  1. 监视器上的解锁操作 happens-before 每个后续的锁定操作。
  2. volatile 字段的写入 happens-before 每个后续的读取操作。
  3. 调用 start() 方法 happens-before 启动线程中的任何操作。
  4. 线程中的所有操作 happens-before 其他线程成功从该线程的 join() 方法返回。
  5. 任何对象的默认初始化 happens-before 程序中的任何其他操作(除了默认写入)。

这些规则都很直观。让我们看一些代码示例。

1. 监视器上的解锁操作 happens-before 每个后续的锁定操作

Java 中的每个对象都有一个内置锁。当我们使用 synchronized 时,我们使用的是对象的锁。

假设我们有一个类和一些方法,并且我们使用对象的锁:

public class HappensBeforeMonitor {
  private int x = 0;
  private int y = 0;

  public void doXY() {
    synchronized(this) {
      x = 1;
      y = 2;
    }
  }
}

假设一个线程调用了 doXY()。对象的锁在获取之前不能被解锁。正如我们之前看到的,synchronized 方法会将代码块包裹在锁和解锁操作之间。任何重新排序优化都不应改变锁操作和解锁操作的顺序。

2. 对 volatile 字段的写入 happens-before 每个后续的读取操作

public class HappensBeforeVolatile {
  private volatile int amount;

  public void update(int newAmount) {
    amount = newAmount;
  }

  public void printAmount() {
    System.out.println(amount);
  }
}

假设 线程 a 调用 update,然后 线程 b 调用 printAmount。读取操作将在写入操作之后发生。写入操作会将值写入主内存。结果是 线程 b 将看到 线程 a 设置的值。

3. 调用 start() 方法 happens-before 启动线程中的任何操作

重新排序不会影响线程启动和线程内部操作之间的顺序。线程内部的所有操作都将在线程启动后发生。

4. 线程中的所有操作 happens-before 其他线程成功从该线程的 join() 方法返回

线程 b 调用 线程 ajoin() 方法。线程 a 中的操作将在 join() 之前发生。当 线程 bjoin() 调用完成时,线程 a 中的更改将对 线程 b 可见。

private int x = 0;
private int y = 1;

public void calculate() throws InterruptedException {
  ...
  final Thread a = new Thread(() -> {
    y = x*y;
  });

  Thread b = new Thread(() -> {
    try {
      a.join();
      System.out.println(y);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  });

  a.start();
  b.start();
  b.join();
  ...
}

5. 任何对象的默认初始化 happens-before 程序中的任何其他操作(除了默认写入)

以这个简单的类为例:

public class HappensBeforeConstructor {
  private final int x;
  private final int y;

  public HappensBeforeConstructor(int a ,int b) {    
    x = a;
    y = b;
  }
}

如果我们仔细想想,实例化的对象继承了 Object.class,就像 Java 中的每个对象一样。如果 Object 的扩展不是隐式的,那么这个类应该是这样的:

public class HappensBeforeConstructor extends Object {
  private final int x;
  private final int y;

  public HappensBeforeConstructor(int a ,int b) {
    super();
    x = a;
    y = b;
  }
}

super(); 方法实例化了对象。这是默认的初始化,构造函数中的其他操作不会被重新排序并在它之前发生。

以上就是 happens-before 规则的简要介绍。在下一篇文章中,我们将探讨内存可见性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值