happens-before原则是什么,有什么用?(非常全面,附带实例)
happens-before(先行发生)是 JMM 中的一个核心概念,它定义了一组规则,用来确定内存操作之间的顺序。
JMM 内存操作必须要满足一定的规则,happens-before 就是定义这些规则的一个等效判断原则。简而言之,如果操作 A happens-before 操作 B,则可以保证操作 A 产生的结果对操作 B 是可见的,即操作 B 不会看到操作 A 的执行结果之前的状态。
happens-before 的作用是解决并发环境下的内存可见性和有序性问题,确保多线程程序的正确性。如果两个操作满足 happens-before 原则,那么不需要进行同步操作,JVM 能够保证操作的有序性,但此时不能随意进行指令重排序;否则,JVM 无法保证操作的有序性,就能进行指令重排序。
happens-before 原则定义的规则具体如下。
例如在同一线程内,如果我们先写入一个变量,再读取同一个变量,那么写入操作 happens-before 读取操作:
例如 synchronized 块,解锁 happens-before 加锁:
例如,如果线程 A 在终止之前修改了一个共享变量,当我们通过 join() 方法等待线程 A 终止或者使用 isAlive() 方法检查到线程 A 已经不再活动时,就可以确信线程 A 中的所有操作都已经执行完毕,包括对共享变量的修改。示例如下:
总之,happens-before 是理解和正确使用 JMM 的关键,通过 happens-before 定义的规则我们可以更好地理解多线程间的内存操作如何互相影响。
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 定义的规则我们可以更好地理解多线程间的内存操作如何互相影响。