首页 > 编程笔记 > Java笔记 阅读:2

happens-before原则是什么,有什么用?(非常全面,附带实例)

happens-before(先行发生)是 JMM 中的一个核心概念,它定义了一组规则,用来确定内存操作之间的顺序。

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 定义的规则我们可以更好地理解多线程间的内存操作如何互相影响。

相关文章