在关于JMM的介绍中,我们知道,JMM 是一种规范,它提供了一系列的机制来保证跨线程的内存可见性、有序性和原子性。
我们之前介绍过很多保证可见性的关键字,如volatile和synchronized等,其实,volatile和synchronized为啥可以保证可见性,也正是因为他们遵守了一个重要的happens-before原则(后文会介绍,Monitor Lock 和Volatile Variable 是happens - before中重要的两个原则)。
我们之前还介绍过一个原则,叫做as-if-serial,他意思指:不管怎么重排序,单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。
✅synchronized是如何保证原子性、可见性、有序性的?
这个as-if-serial语义是针对单线程的,但是如果在多线程情况下呢?有没有什么原则可以保证有序性呢?这就需要我们的happens-before原则了。
happens-before原则是一种用于描述多线程程序中操作执行顺序的规则。它是Java内存模型(Java Memory Model,JMM)的一部分:如果一个操作 A “happen-before” 另一个操作 B,那么 A 的结果对 B 是可见的。这个概念是理解线程间内存可见性的关键。
举一个例子,如以下代码:
public class ThreadStartExample {
private int startValue = 10;
public void startNewThread() {
startValue +=1;
new Thread(() -> {
int localValue = startValue;
}).start();
}
}
有两个线程,一个写startValue,一个读startValue,但是我们并没有用synchronized加锁,也没有用volatile修饰,那么,JVM是如何保证,在主线程中修改startValue的操作在子线程中是可见的呢?
这其实就是happens-before原则发挥的作用了。但是,happens-before原则也不是没有任何限制,任何场景都能happens-before的,还是有一些规则要求的。我们接下来介绍下每个规则以及附上一些代码演示
以下十几个happends-before原则的适用场景(节选自《深入理解Java虚拟机》,并做了一些描述上的修改,增加了代码示例,方便大家理解)
public class ProgramOrderExample {
private int a = 0;
private int b = 0;
public void method() {
a = 1; // 操作1
b = 2; // 操作2,
// 操作1 `happens-before` 操作2
}
}
public class MonitorLockExample {
private int value = 0;
private final Object lock = new Object();
public void increment() {
synchronized(lock) {
value++; // 操作在锁内
}
}
public int getValue() {
synchronized(lock) {
return value; // 此操作 `happens-before` increment() 中的操作
}
}
}
increment 方法中对 value 的修改,在 getValue 方法获取锁之后是可见的。
public class VolatileExample {
private volatile boolean flag = false;
public void writeFlag() {
flag = true; // volatile 写操作
}
public boolean checkFlag() {
return flag; // 这里读取到的 flag 值 `happens-before` 写操作
}
}
当一个线程调用 writeFlag(),另一个线程随后调用 checkFlag() 将看到 flag 为 true。
public class ThreadStartExample {
private int startValue = 10;
public void startNewThread() {
startValue +=1;
new Thread(() -> {
int localValue = startValue; // startValue 的值 `happens-before` 这里的读操作
// 处理 localValue
}).start();
}
}
线程启动时,将看到 startValue 的值为 11。
public class ThreadJoinExample {
private int counter = 0;
public void incrementInThread() throws InterruptedException {
Thread thread = new Thread(() -> {
counter++;
});
thread.start();
thread.join(); // `happens-before` counter 的读操作
int value = counter; // 这里能够看到线程中对 counter 的修改
}
}
主线程中可以看到子线程对counter的修改
线程中断规则(Thread Interruption Rule): 对线程的 interrupt() 方法的调用 happens-before 被中断线程检测到中断事件的发生。即线程的中断操作在被该线程检测到之前已经发生。
对象终结规则(Finalizer Rule): 一个对象的初始化完成(构造函数执行结束)happens-before 它的 finalize() 方法的开始。即在对象被回收前,其构造过程已经完全结束。
public class FinalizerExample {
private int value;
public FinalizerExample() {
value = 10; // 构造函数中的操作
}
@Override
protected void finalize() {
// 此方法 `happens-before` 构造函数中的操作
if (value != 10) {
// 这里不应该发生
}
}
}
public class TransitivityExample {
private volatile boolean ready;
private int number;
public void writer() {
number = 42; // 操作 A
ready = true; // 操作 B
}
public void reader() {
if (ready) { // 操作 C(由于 ready 是 volatile,它 `happens-before` 这里的操作)
assert number == 42; // 因为 A `happens-before` B,B `happens-before` C,所以 A `happens-before` C
}
}
}
由于 ready 是一个 volatile 变量,写入 ready(操作 B)发生在读取 ready 之前,同样,写入 number(操作 A)发生在写入 ready 之前。根据传递性规则,写入 number 发生在读取 ready 之前。