在 JMM 中,定义了一套 Happens-Before 原则,用于保证程序在执行过程中的可见性和有序性。Happens-Before 原则主要包括程序次序原则、volatile 变量原则、传递原则、锁定原则、线程启动原则、线程总结原则、线程中断原则和对象终结原则。
在JMM中, 如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
换个角度来说,如果 A happens-before B,则意味着 A 的执行结果必须对 B 可见,也就是保证跨线程的内存可见性。
程序次序原则表示在单个线程中,程序按照代码的顺序执行,前面的代码操作必然发生于后面的代码操作之前。
例如如下代码:
/**
* 程序次序原则
*/
public void programOrder(){
// 1
int a = 1;
// 2
int b = 2;
// 3
int sum = a + b;
}
在单个线程中,会按照代码的书写顺序依次执行 1、2、3 三行代码。
volatile 变量原则表示对一个 volatile 变量写的操作,必然发生于后续对这个变量的读操作之前。
例如如下代码:
private volatile int count = 0;
private double amount = 0;
/**
* volatile变量写规则
*/
public void writeAmountAndCount(){
amount = 1;
count = 1;
}
/**
* volatile变量读规则
*/
public void readAmountAndCount(){
if (count == 1){
System.out.println(amount);
}
}
在上述代码中,先将 volatile 变量 count 和普通变量 amount 都赋值为 0,然后在 writeAmountAndCount 方法中将 amount 赋值为 1,将 count 赋值为 1。则在 readAmountAndCount 方法中 count 的值等于 1 的前提下,amount 的值一定为 1。
传递原则表示如果操作 A 先于 操作 B 发生,操作 B 又先于操作 C 发生,则操作 A 一定先于操作 C 发生。
锁定原则表示对一个锁的解锁操作必然发生于后续对这个锁的加锁操作之前。
例如如下代码
private int value = 0;
/**
* 锁定原则
*/
public void synchrionizedUpdateValue(){
synchronized (this){
if (value < 1){
value = 1;
}
}
}
在上述代码中,初始 value 为 0,当线程 1 执行 synchrionizedUpdateValue 方法时,对方法进行加锁,判断 value 小于 1 成立,则将 value 修改为 1,随后线程 1 释放锁。当线程 2 执行 synchrionizedUpdateValue 方法时,读到的 value 为 1,判断 value 小于 1 不成立,则释放锁。也就是说,线程 1 释放锁的操作先于线程 2 的加锁操作。
线程启动原则表示如果线程 1 调用线程 2 的 start 方法启动线程 2,则 start 方法操作必然发生于线程 2 中的任意操作之前。
例如如下代码
private int value = 0;
/**
* 线程启动原则
*/
public void threadStart(){
Thread thread2 = new Thread(()-> {
System.out.println(value);
});
value = 10;
thread2.start();
}
在上述代码中,初始 value 为 0,虽然在 threadStart 方法中先创建了 thread2 对象实例,但是由于在调用 thread2 的 start 方法之前,将 value 赋值为 10,所以在 thread2 线程中打印的 value 为 10.
线程终结原则表示如果线程 1 等待线程 2 完成操作,那么当线程 2 完成后,线程 1 能够访问到线程 2 修改后的共享变量的值。
例如如下代码
private int value = 0;
/**
* 线程终结原则
*/
public void threadEnd() throws InterruptedException {
Thread thread2 = new Thread(()-> {
value = 10;
});
thread2.start();
thread2.join();
System.out.println(value);
}
在上述代码中,初始 value 为 0,在 threadEnd 方法中,先创建 thread2 对象,并在 thread2 线程中将 value 赋为 10,随后调用 therad2 的 start 方法启动 thread2 线程,再调用 thread2 的 join 方法等待 thread2 线程执行完毕。随后打印 value 为 10。
线程中断原则表示对线程 interrupt 方法的调用必然发生于被中断线程的代码检测到中断事件发生前。
例如如下代码
private int value = 0;
/**
* 线程中断原则
*/
public void threadInterrupt() throws Exception{
Thread thread2 = new Thread(()->{
if(Thread.currentThread().isInterrupted()){
System.out.println(value);
}
});
thread2.start();
value = 10;
thread2.interrupt();
}
在上述代码中,初始 value 为 0,在 threadInterrupt 方法中,先创建 thread2 对象,在 thread 线程中判断当前线程是否被中断,如果已经中断,则打印 value。随后启动 thread2 线程,将 value 修改为 10,再中断 thread2 线程。则在 thread2 中,检测到当前线程被中断时,打印的 value 为 10。
对象终结原则表示一个对象的初始化必然发生于它的 finalize 方法开始前。
例如如下代码
public class HappensBeforeTest {
/**
* 对象的终结原则
*/
public HappensBeforeTest(){
System.out.println("执行构造方法");
}
@Override
protected void finalize() throws Throwable {
System.out.println("执行finalize()方法");
}
public static void main(String[] args){
new HappensBeforeTest();
//通知JVM执行GC,不一定立刻执行
System.gc();
}
}
在上述代码中,在 HappensBeforeTest 类中的构造方法中打印了“执行构造方法”,在 finalize 方法中打印了“执行finalize()方法”。在 main 方法中首先调用了 HappensBeforeTest 类构造方法创建对象实例,随后调用“ System.gc();”通知 JVM 执行 GC 操作,但 JVM 不一定立刻执行。运行结构如下:
执行构造方法
执行finalize()方法
说明一个对象的初始化发生于它的 finalize 方法开始前。