- 冲刺高薪Offer:Java并发编程进阶及面试指南
- 吴晓勇 梁建全编著
- 1468字
- 2025-03-13 18:03:40
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定义的规则我们可以更好地理解多线程间的内存操作如何互相影响。