当前位置: 首页 > news >正文

深入浅出Java中的Happens-Before原则!

在这里插入图片描述

目录

    • 前言
    • 一、Happens-Before是什么?为什么需要它?
      • 1.1 从一个问题说起
      • 1.2 Happens-Before的定义
      • 1.3 为什么需要Happens-Before?
    • 二、Happens-Before的核心规则
      • 规则1:程序顺序规则(Program Order Rule)
      • 规则2:监视器锁规则(Monitor Lock Rule)
      • 规则3:volatile变量规则(Volatile Variable Rule)
      • 规则4:线程启动规则(Thread Start Rule)
      • 规则5:线程终止规则(Thread Termination Rule)
      • 规则6:线程中断规则(Thread Interruption Rule)
      • 规则7:传递性规则(Transitivity)
      • 规则8:对象终结规则(Finalizer Rule)
    • 三、Happens-Before与重排序的关系
    • 四、不满足Happens-Before的情况:可见性问题
    • 五、Happens-Before的实际应用
      • 5.1 synchronized与Happens-Before
      • 5.2 volatile与Happens-Before
      • 5.3 ConcurrentHashMap与Happens-Before
    • 六、常见面试问题
    • 七、总结

🌟我的其他文章也讲解的比较有趣😁,如果喜欢博主的讲解方式,可以多多支持一下,感谢🤗!

其他优质专栏: 【🎇SpringBoot】【🎉多线程】【🎨Redis】【✨设计模式专栏】…等

如果喜欢作者的讲解方式,可以点赞收藏加关注,你的支持就是我的动力
✨更多文章请看个人主页: 码熔burning

前言

在Java并发编程中,我们经常会遇到这样的问题:多线程环境下,一个线程对共享变量的修改,另一个线程能看到吗?为什么有时候明明修改了变量,其他线程却读取到旧值?这些问题的答案,都与Java内存模型(JMM)中的Happens-Before原则密切相关。

本文将从基础概念出发,循序渐进地解析Happens-Before的定义、核心规则及实际应用,帮助你彻底理解这一Java并发编程的基石。

一、Happens-Before是什么?为什么需要它?

1.1 从一个问题说起

先看一段简单的代码:

// 线程A执行
int x = 1;  // 操作A
boolean flag = true;  // 操作B// 线程B执行
if (flag) {  // 操作CSystem.out.println(x);  // 操作D
}

如果线程A和线程B并发执行,线程D会打印出1吗?

直觉上应该会,但实际上可能不会。因为在多线程环境中,编译器优化、CPU指令重排序、缓存等机制可能导致:

  • 线程A中,操作A和B的执行顺序可能被调换(重排序)
  • 线程A修改的x和flag可能还停留在CPU缓存中,未同步到主内存
  • 线程B可能读取的是主内存中未更新的旧值

这些问题会导致可见性有序性问题。而Happens-Before原则就是JMM为解决这些问题提出的核心规范。

1.2 Happens-Before的定义

Happens-Before是JMM中定义的两个操作之间的偏序关系
如果操作A Happens-Before操作B,那么A的执行结果必须对B可见,且A的执行顺序在逻辑上先于B。

注意

  • Happens-Before不代表“物理时间上的先后”,而是JMM定义的“逻辑先行”关系。
  • 即使A在物理时间上后于B执行,只要A Happens-Before B,A的结果就必须对B可见。

1.3 为什么需要Happens-Before?

JMM的核心目标是:在保证并发程序正确性的前提下,尽可能为编译器和CPU的优化留出空间

Happens-Before的价值在于:

  • 它屏蔽了底层硬件和编译器的复杂细节(如重排序、缓存等),为开发者提供了简单清晰的可见性判断标准
  • 它允许编译器和CPU在不违反Happens-Before规则的前提下进行优化,保证性能。

简单说:开发者只需关注是否满足Happens-Before规则,无需关心底层如何实现可见性;JVM则需保证在满足规则的情况下,底层优化不破坏可见性

二、Happens-Before的核心规则

JMM定义了8条核心的Happens-Before规则,这些规则是判断可见性的基础。我们逐一解析:

规则1:程序顺序规则(Program Order Rule)

在同一个线程中,按照代码顺序,前面的操作Happens-Before后面的操作

例如:

// 单线程内
int a = 1;  // 操作A
int b = a + 1;  // 操作B

根据程序顺序规则,A Happens-Before B。因此:

  • 操作A的结果(a=1)对操作B可见
  • 逻辑上A先于B执行(即使编译器可能重排序,但会保证结果等价于顺序执行)

规则2:监视器锁规则(Monitor Lock Rule)

对一个锁的解锁操作Happens-Before后续对同一个锁的加锁操作

synchronized是Java中最典型的监视器锁,例如:

synchronized (lock) {  // 加锁// 临界区操作Ax = 1;
}  // 解锁(操作U)// 其他线程
synchronized (lock) {  // 加锁(操作L)// 临界区操作BSystem.out.println(x);  // 必然打印1
}

根据规则:
解锁操作U Happens-Before 后续的加锁操作L,因此操作A的结果(x=1)对操作B可见。

这就是synchronized能保证可见性的底层原因。

规则3:volatile变量规则(Volatile Variable Rule)

对volatile变量的写操作Happens-Before后续对同一个volatile变量的读操作

例如:

// 线程A
volatile int x = 0;
x = 1;  // 写操作W// 线程B
int y = x;  // 读操作R

根据规则:W Happens-Before R,因此线程B读取到的y必然是1(而非0)。

原理
volatile变量的写操作会强制将缓存中的值刷新到主内存,读操作会强制从主内存加载最新值,且禁止了volatile变量前后操作的重排序,从而保证可见性。

规则4:线程启动规则(Thread Start Rule)

主线程对Thread对象的start()方法调用Happens-Before子线程中的所有操作

例如:

// 主线程
int x = 1;
Thread t = new Thread(() -> {// 子线程操作System.out.println(x);  // 必然打印1
});
t.start();  // 启动操作S

根据规则:S Happens-Before子线程中的打印操作,因此主线程在start()前对x的修改(x=1)对sub线程可见。

规则5:线程终止规则(Thread Termination Rule)

子线程中的所有操作Happens-Before主线程检测到子线程终止

主线程可以通过join()isAlive()等方法检测子线程是否终止,例如:

// 主线程
Thread t = new Thread(() -> {// 子线程操作Ax = 1;
});
t.start();
t.join();  // 等待子线程终止(操作J)
System.out.println(x);  // 必然打印1

根据规则:子线程的操作A Happens-Before 主线程的操作J,因此主线程在join()后能看到x=1。

规则6:线程中断规则(Thread Interruption Rule)

对线程interrupt()方法的调用Happens-Before被中断线程检测到中断事件

例如:

// 线程A
Thread t = new Thread(() -> {// 子线程if (Thread.interrupted()) {  // 检测中断(操作C)System.out.println("被中断");}
});
t.start();
t.interrupt();  // 中断操作I

根据规则:I Happens-Before C,因此子线程能检测到中断事件。

规则7:传递性规则(Transitivity)

如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C

这是非常重要的一条规则,它可以将多个Happens-Before关系串联起来,例如:

// 线程A
x = 1;  // A
volatile boolean flag = true;  // B// 线程B
if (flag) {  // C(读volatile)int y = x;  // D
}
  • 根据程序顺序规则:A Happens-Before B,C Happens-Before D
  • 根据volatile规则:B Happens-Before C
  • 根据传递性:A Happens-Before D → 因此D能看到x=1

规则8:对象终结规则(Finalizer Rule)

对象的构造函数执行完毕Happens-Before其finalize()方法开始执行

确保对象在被回收前,其构造函数的所有初始化操作都已完成,例如:

class MyObject {int x;MyObject() {x = 1;  // 构造函数操作A}@Overrideprotected void finalize() {System.out.println(x);  // 必然打印1(操作B)}
}

根据规则:A Happens-Before B,因此finalize()中能看到构造函数对x的初始化。

三、Happens-Before与重排序的关系

JMM允许编译器和CPU进行重排序优化,但有一个前提:重排序不能破坏Happens-Before规则

例如,在单线程中:

int a = 1;  // A
int b = 2;  // B
int c = a + b;  // C

编译器可能将A和B重排序(先执行B再执行A),但由于A和B之间没有数据依赖,且重排序后C的结果仍为3,因此这种重排序是允许的——它没有破坏程序顺序规则的Happens-Before关系(A和B的执行顺序不影响最终结果的可见性)。

但如果是:

int a = 1;  // A
int b = a;  // B

A和B存在数据依赖(B依赖A的结果),编译器不能重排序A和B,否则会破坏程序顺序规则的Happens-Before关系(B可能读取到a的旧值)。

四、不满足Happens-Before的情况:可见性问题

如果两个操作之间不存在任何Happens-Before规则,JMM无法保证它们的可见性,可能出现“脏读”。

例如:

// 线程A
int x = 1;  // A// 线程B
System.out.println(x);  // B

A和B之间没有任何Happens-Before关系(不满足上述8条规则中的任何一条),因此:

  • 线程B可能打印1(x已同步到主内存)
  • 也可能打印0(x仍在A的CPU缓存中,未同步)

这种情况下,结果是不确定的,这就是多线程编程中“可见性问题”的根源。

五、Happens-Before的实际应用

Happens-Before是理解Java并发工具的基础,以下是几个典型场景:

5.1 synchronized与Happens-Before

synchronized通过“解锁-加锁”的Happens-Before关系保证可见性:

// 线程A
synchronized (lock) {x = 1;  // 解锁前的操作
}  // 解锁U// 线程B
synchronized (lock) {  // 加锁L(U Happens-Before L)System.out.println(x);  // 可见x=1
}

5.2 volatile与Happens-Before

volatile通过“写-读”的Happens-Before关系保证可见性:

// 线程A
volatile boolean ready = false;
int data = 0;data = 1;  // A
ready = true;  // B(写volatile)// 线程B
if (ready) {  // C(读volatile,B Happens-Before C)System.out.println(data);  // D(A Happens-Before D,因此可见1)
}

这里结合了程序顺序规则(A Happens-Before B)、volatile规则(B Happens-Before C)和传递性规则(A Happens-Before D)。

5.3 ConcurrentHashMap与Happens-Before

ConcurrentHashMap的put操作与get操作之间存在Happens-Before关系:

  • 线程A的put(k, v)操作Happens-Before线程B的get(k)操作
  • 因此线程B的get(k)能看到线程Aput的v值

这是ConcurrentHashMap保证线程安全的底层基础之一。

六、常见面试问题

  1. Happens-Before的定义是什么?
    它是JMM中定义的两个操作之间的偏序关系:如果A Happens-Before B,则A的结果对B可见,且A的逻辑执行顺序先于B。

  2. Happens-Before和物理时间顺序有什么区别?
    无关。Happens-Before是逻辑先行关系,与物理时间上的先后无关。即使A在物理时间上后于B执行,只要A Happens-Before B,A的结果就必须对B可见。

  3. volatile变量的写操作和读操作之间有什么Happens-Before关系?
    对volatile变量的写操作Happens-Before后续对该变量的读操作,这保证了volatile变量的可见性。

  4. 如何利用Happens-Before规则判断多线程操作的可见性?
    只要两个操作之间存在通过Happens-Before规则(直接或间接)建立的关系,就可以保证可见性;否则,可见性无法保证。

七、总结

Happens-Before是Java内存模型的核心,它为开发者提供了判断多线程操作可见性的清晰标准。理解Happens-Before,你就能:

  • 明白为什么synchronizedvolatile等关键字能保证可见性
  • 避免多线程编程中的“脏读”“不可见”等问题
  • 更深入地理解Java并发工具(如ConcurrentHashMap、AQS)的底层原理

记住:Happens-Before的本质是“可见性契约”——JMM通过它承诺,只要满足规则,就保证操作结果的可见性。掌握这一原则,是成为Java并发编程高手的关键一步。


文章转载自:

http://5jP6VTI3.rytps.cn
http://tPjTb8wU.rytps.cn
http://l57yHCxv.rytps.cn
http://klQhuEz6.rytps.cn
http://z2sJ9bcq.rytps.cn
http://VeLJN2uh.rytps.cn
http://VoGketvJ.rytps.cn
http://EowjL947.rytps.cn
http://HSmjtQhm.rytps.cn
http://RFHr2HJW.rytps.cn
http://PTzIPKNx.rytps.cn
http://DzrTKm39.rytps.cn
http://RIXd978G.rytps.cn
http://8KQ0Garn.rytps.cn
http://N51z2rTG.rytps.cn
http://1Vkn7Gz0.rytps.cn
http://UseUIhkR.rytps.cn
http://GzH9CW5H.rytps.cn
http://yMgRqXBU.rytps.cn
http://IxDI1wM7.rytps.cn
http://CIrwUUis.rytps.cn
http://Ix90G89C.rytps.cn
http://qEu9mFQx.rytps.cn
http://Xb8VcvlD.rytps.cn
http://m4CfXAKD.rytps.cn
http://Ky5r6y04.rytps.cn
http://vZboa2SW.rytps.cn
http://KPkCS18x.rytps.cn
http://OjAPc0GS.rytps.cn
http://SlxD44s3.rytps.cn
http://www.dtcms.com/a/388264.html

相关文章:

  • centos7更换yum源
  • [特殊字符] 认识用户手册用户手册(也称用户指南、产品手册)是通过对产品功能的清
  • Codex 在 VS Code/Cursor 的插件基础配置
  • 前端Web案例-登录退出
  • Redis学习------------缓存优化
  • openfeigin 跨服务调用流程 源码阅读
  • 运动手环心率监测:原理、可靠性与市场顶尖之选全解析​​
  • 端到端智驾测试技术论文阅读
  • Frank-Wolfe算法:深入解析与前沿应用
  • GPT-5-Codex CLI保姆级教程:获取API Key配置与openai codex安装详解
  • 代码优化测试
  • 深度学习基础:PyTorch张量创建与操作详解
  • 7 大文献综述生成工具 2025 实测推荐
  • 红黑树 详解
  • 第十六章 Arm C1-Premium核心调试系统深度解析
  • Python压缩数据文件读写完全指南:从基础到高并发实战
  • HTTP/1.0 与 HTTP/2.0 的主要区别
  • 颜群JVM【02】JVM运行时的内存区域
  • 自定义Grafana错误率面板No Data问题排查
  • 深入剖析C++内存模型:超越原子性的多线程编程基石
  • 彻底禁用移动端H5页面默认下拉刷新功能
  • GPT-5-Codex深度解析:动态推理分配的编程AI如何改变软件开发
  • 代码审计-PHP专题MVC开发控制视图URL路由文件定位SQL注入文件安全1day分析
  • npm install 报错 proxy...connect ECONNREFUSED 127.0.0.1:xxxx
  • 第九章 Arm C1-Premium 核心内部内存直接访问指南
  • 微信小程序-7-wxml常用语法和发送网络请求
  • 数据结构9——树
  • 第三方软件测评机构:【Python Requests库实战教学】
  • 信用违约风险分类预测:XGBoost +SHAP实践案例
  • TypeScript 基础