通常,当我们写代码时,我们会假设代码是按照编写的顺序执行的。但事实并非如此,因为出于优化目的,语句的重新排序可能会在编译时或运行时发生。
无论线程何时运行程序,结果都应该像是所有操作都按照它们在程序中出现的顺序发生一样。单线程程序的执行应遵循“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 保证。
规则
我们可以查看文档了解这些规则。
- 监视器上的解锁操作 happens-before 每个后续的锁定操作。
- 对
volatile字段的写入 happens-before 每个后续的读取操作。 - 调用
start()方法 happens-before 启动线程中的任何操作。 - 线程中的所有操作 happens-before 其他线程成功从该线程的
join()方法返回。 - 任何对象的默认初始化 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 调用 线程 a 的 join() 方法。线程 a 中的操作将在 join() 之前发生。当 线程 b 的 join() 调用完成时,线程 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 规则的简要介绍。在下一篇文章中,我们将探讨内存可见性。

973

被折叠的 条评论
为什么被折叠?



