1.2.4 什么是happens-before原则?它有什么作用?

happens-before(先行发生)是JMM中的一个核心概念,它定义了一组规则,用来确定内存操作之间的顺序。1.2.3小节讲到的JMM内存操作必须要满足一定的规则,happens-before就是定义这些规则的一个等效判断原则。简而言之,如果操作A happens-before操作B,则可以保证操作A产生的结果对操作B是可见的,即操作B不会看到操作A的执行结果之前的状态。

happens-before的作用是解决并发环境下的内存可见性和有序性问题,确保多线程程序的正确性。如果两个操作满足happens-before原则,那么不需要进行同步操作,JVM能够保证操作的有序性,但此时不能随意进行指令重排序;否则,JVM无法保证操作的有序性,就能进行指令重排序。

happens-before原则定义的规则具体如下。

(1)程序代码顺序规则。

在同一个线程中,按照程序代码顺序,前面的操作发生在后面的操作之前。例如在同一线程内,如果我们先写入一个变量,再读取同一个变量,那么写入操作happens-before读取操作。

int x=0;//写入操作
int y=x;//读取操作,这里能看到x=0

注意,程序代码顺序要考虑分支、循环等结构,因此该顺序确切来讲应该是程序控制流顺序。

(2)监视器锁规则。

解锁发生在加锁之前,且必须针对同一个锁。例如synchronized块,解锁happens-before加锁。

synchronized(lock) {
   sharedVar = 1; // 在锁内的写入操作
}//lock解锁happens-before加锁
 
synchronized(lock) {
   int r = sharedVar; // 在另一个锁内的读取操作,这里能看到sharedVar=1
}

(3)volatile变量规则。

对一个volatile变量的写入操作发生在读取操作之前,示例如下。

volatile int flag = 0;
// 线程A
flag = 1; // 写入操作
 
// 线程B
int f = flag; // 读取操作,这里能看到flag=1

(4)线程启动规则。

Thread对象的start()方法发生在线程的每一个后续操作之前,示例如下。

Thread t = new Thread(new Runnable() {
   public void run() {
       int readX = x; // 线程中的任何操作,能看到start()之前的写入操作
   }
});
 
x = 10; // 主线程写入操作
t.start(); // start() happens-before子线程中的所有操作

(5)线程终止规则。

线程中的所有操作,例如读取、写入和加锁等,都发生在这个线程终止之前,也就是说,当我们观察到一个线程终止时,就可以确认该线程的所有操作都已经完成了。例如,如果线程A在终止之前修改了一个共享变量,当我们通过join()方法等待线程A终止或者使用isAlive()方法检查到线程A已经不再活动时,就可以确信线程A中的所有操作都已经执行完毕,包括对共享变量的修改。示例如下。

Thread threadA = new Thread(() -> {
    // 这里是线程 A 的操作
    someSharedVariable = 123; // 对共享变量的写入操作
});
 
threadA.start();  // 启动线程 A
threadA.join();   // 等待线程 A 终止
 
// 当 threadA.join() 结束后
// 可以确信threadA对someSharedVariable 的写入操作已经完成
assert someSharedVariable == 123; // 这里可以安全地检查共享变量的值

在上述代码中,使用assert表达式检查someSharedVariable是否为123是安全的,因为threadA.join()保证了所有线程A中的操作在主线程观察到线程A终止之前都已经完成。

(6)线程中断规则。

对一个线程调用interrupt()方法,实际上是设置了该线程的中断状态,主线程的interrupt()调用发生在子线程检测到中断之前,示例如下。

Thread t = new Thread(new Runnable() {
   public void run() {
     while (!Thread.currentThread().isInterrupted()) {
       // 业务处理逻辑
     }
     // 能看到中断状态
   }
});
t.start();
t.interrupt(); // 主线程的interrupt()调用发生在子线程检测到中断之前

(7)对象终结规则。

一个对象的初始化完成,即构造函数的执行完成,发生在finalize()方法之前,示例如下。

public class ExampleObject {
   private int x;
   public ExampleObject() {
     x = 10; // 构造函数的写操作
   }
   protected void finalize() {
     int readX = x; // 在finalize()中,可以看到构造函数的写操作结果
   }
}

(8)传递性。

如果A操作发生在B操作之前,且B操作发生在C操作之前,则A操作发生在C操作之前,示例如下。

volatile int flag = 0;
int a = 0;
// 线程A
a = 1; // A操作
flag = 1; // B操作
 
// 线程B
if (flag == 1) { // C操作
    int readA = a; // 这里可以保证readA = 1,因为A happens-before B happens-before C
}

上述这些规则,为Java程序员在多线程环境中编写线程安全的代码提供了一个清晰的框架。通过理解和运用这些规则,可以避免数据竞争和内存一致性错误。

总之,happens-before是理解和正确使用JMM的关键,通过happens-before定义的规则我们可以更好地理解多线程间的内存操作如何互相影响。