千家信息网

Happens-before的作用是什么

发表于:2026-11-03 作者:千家信息网编辑
千家信息网最后更新 2026年11月03日,这篇文章主要介绍"Happens-before的作用是什么",在日常操作中,相信很多人在Happens-before的作用是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家
千家信息网最后更新 2026年11月03日Happens-before的作用是什么

这篇文章主要介绍"Happens-before的作用是什么",在日常操作中,相信很多人在Happens-before的作用是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"Happens-before的作用是什么"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

写在前面

上一篇文章并发 Bug 之源有三,请睁大眼睛看清它们 谈到了可见性/原子性/有序性三个问题,这些问题通常违背我们的直觉和思考模式,也就导致了很多并发 Bug

  • 为了解决 CPU,内存,IO 的短板,增加了缓存,但这导致了可见性问题

  • 编译器/处理器擅自优化 ( Java代码在编译后会变成 Java 字节码, 字节码被类加载器加载到 JVM 里, JVM 执行字节码, 最终需要转化为汇编指令在 CPU 上执行) ,导致有序性问题

初衷是好的,但引发了新问题,最有效的办法就禁止缓存和编译优化,问题虽然能解决,但「又回到最初的起点,呆呆地站在镜子前」是很尴尬的,我们程序的性能就堪忧了.

解决方案

  1. 作为我们程序猿不想写出 bug 影响 KPI,所以希望内存模型易于理解、易于编程。这就需要基于一个强内存模型来编写代码

  2. 作为编译器和处理器不想让外人说它处理速度很慢,所以希望内存模型对他们束缚越少越好,可以由他们擅自优化,这就需要基于一个弱内存模型

俗话说:「没有什么事是开会解决不了的,如果有,那就再开一次」????

JSR-133 的专家们就有了新想法,既然不能完全禁止缓存和编译优化,那就按需禁用缓存和编译优化,按需就是要加一些约束,约束中就包括了上一篇文章简单提到过的 volatile,synchronized,final 三个关键字,同时还有你可能听过的 Happens-Before 原则(包含可见性和有序性的约束),Happens-before 规则也是本章的主要内容

为了满足二者的强烈需求,照顾到双方的情绪,于是乎: JMM 就对程序猿说了一个善意的谎言: 「会严格遵守 Happpen-Befores 规则,不会重排序」让程序猿放心,私下却有自己的策略:

  1. 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。

  2. 对于不会改变程序执行结果的重排序, JMM对编译器和处理器不做要求 (JMM允许这种重排序)。

我们来用个图说明一下:

这就是那个善意的谎言,虽是谎言,但还是照顾到了程序猿的利益,所以我们只需要了解 happens-before 规则就能得到保证 (图画了好久,不知道是否说明了谎言的所在????,欢迎留言)

Happens-before

Happens-before 规则主要用来约束两个操作,两个操作之间具有 happens-before 关系, 并不意味着前一个操作必须要在后一个操作之前执行,happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见, (the first is visible to and ordered before the second)

说了这么多,先来看一小段代码带你逐步走进 Happen-Befores 原则,看看是怎样用该原则解决 可见性 和 有序性 的问题:

class ReorderExample {  int x = 0;  boolean flag = false;  public void writer() {    x = 42;    //1    flag = true;    //2  }  public void reader() {    if (flag) { //3      System.out.println(x);    //4    }  }}

假设 A 线程执行 writer 方法,B 线程执行 reader 方法,打印出来的 x 可能会是 0,上一篇文章说明过: 因为代码 1 和 2 没有数据依赖关系,所以可能被重排序

flag = true;    //2x = 42;    //1

所以,线程 A 将 flag = true 写入但没有为 x 重新赋值时,线程 B 可能就已经打印了 x 是 0

那么为 flag 加上 volatile 关键字试一下:

volatile boolean flag = false;

即便加上了 volatile 关键字,这个问题在 java1.5 之前还是没有解决,但 java1.5 和其之后的版本对 volatile 语义做了增强,问题得以解决,这就离不开 Happens-before 规则的约束了,总共有 6 个规则,且看

程序顺序性规则

一个线程中的每个操作, happens-before 于该线程中的任意后续操作 第一感觉这个原则是一个在理想状态下的"废话",并且和上面提到的会出现重排序的情况是矛盾的,注意这里是一个线程中的操作,其实隐含了「as-if-serial」语义: 说白了就是只要执行结果不被改变,无论怎么"排序",都是对的

这个规则是一个基础规则,happens-before 是多线程的规则,所以要和其他规则约束在一起才能体现出它的顺序性,别着急,继续向下看

volatile变量规则

对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读

我将上面的程序添加两行代码作说明:

public class ReorderExample {        private int x = 0;        private int y = 1;        private volatile boolean flag = false;        public void writer(){                x = 42;       //1                y = 50;       //2                flag = true;  //3        }        public void reader(){                if (flag){ //4                        System.out.println("x:" + x);       //5                        System.out.println("y:" + y);       //6                }        }}

这里涉及到了 volatile 的内存增强语义,先来看个表格:

能否重排序第二个操作第二个操作第二个操作
第一个操作普通读/写volatile 读volatile 写
普通读/写--NO
volatile 读NONONO
volatile 写-NONO

从这个表格 最后一列 可以看出:

如果第二个操作为 volatile 写,不管第一个操作是什么,都不能重排序,这就确保了 volatile 写之前的操作不会被重排序到 volatile 写之后 拿上面的代码来说,代码 1 和 2 不会被重排序到代码 3 的后面,但代码 1 和 2 可能被重排序 (没有依赖也不会影响到执行结果),说到这里和 程序顺序性规则是不是就已经关联起来了呢?

从这个表格的 倒数第二行 可以看出:

如果第一个操作为 volatile 读,不管第二个操作是什么,都不能重排序,这确保了 volatile 读之后的操作不会被重排序到 volatile 读之前 拿上面的代码来说,代码 4 是读取 volatile 变量,代码 5 和 6 不会被重排序到代码 4 之前

volatile 内存语义的实现是应用到了 「内存屏障」,因为这完全够单独写一章的内容,这里为了不掩盖主角 Happens-before 的光环,保持理解 Happens-before 的连续性,先不做过多说明

到这里,看这个规则,貌似也没解决啥问题,因为它还要联合第三个规则才起作用

传递性规则

如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C 直接上图说明一下上面的例子

从上图可以看出

  • x =42 和 y = 50 Happens-before flag = true, 这是规则 1

  • 写变量(代码 3) flag=true Happens-before 读变量(代码 4) if(flag),这是规则 2

根据规则 3传递性规则,x =42 Happens-before 读变量 if(flag)

谜案要揭晓了: 如果线程 B 读到了 flag 是 true,那么 x =42 和 y = 50 对线程 B 就一定可见了,这就是 Java1.5 的增强 (之前版本是可以普通变量写和 volatile 变量写的重排序的)

通常上面三个规则是一种联合约束,到这里你懂了吗?规则还没完,继续看

监视器锁规则

对一个锁的解锁 happens-before 于随后对这个锁的加锁

这个规则我觉得你应该最熟悉了,就是解释 synchronized 关键字的,来看

public class SynchronizedExample {        private int x = 0;        public void synBlock(){                // 1.加锁                synchronized (SynchronizedExample.class){                        x = 1; // 对x赋值                }                // 3.解锁        }        // 1.加锁        public synchronized void synMethod(){                x = 2; // 对x赋值        }        // 3. 解锁}

先获取锁的线程,对 x 赋值之后释放锁,另外一个再获取锁,一定能看到对 x 赋值的改动,就是这么简单,请小伙伴用下面命令查看上面程序,看同步块和同步方法被转换成汇编指令有何不同?

javap -c -v SynchronizedExample

这和 synchronized 的语义相关,小伙伴可以先自行了解一下,锁的内容时会做详细说明

start()规则

如果线程 A 执行操作 ThreadB.start() (启动线程B), 那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作,也就是说,主线程 A 启动子线程 B 后,子线程 B 能看到主线程在启动子线程 B 前的操作,看个程序就秒懂了

public class StartExample {        private int x = 0;        private int y = 1;        private boolean flag = false;        public static void main(String[] args) throws InterruptedException {                StartExample startExample = new StartExample();                Thread thread1 = new Thread(startExample::writer, "线程1");                startExample.x = 10;                startExample.y = 20;                startExample.flag = true;                thread1.start();                System.out.println("主线程结束");        }        public void writer(){                System.out.println("x:" + x );                System.out.println("y:" + y );                System.out.println("flag:" + flag );        }}

运行结果:

主线程结束x:10y:20flag:trueProcess finished with exit code 0

线程 1 看到了主线程调用 thread1.start() 之前的所有赋值结果,这里没有打印「主线程结束」,你知道为什么吗?这个守护线程知识有关系

join()规则

如果线程 A 执行操作 ThreadB.join() 并成功返回, 那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join() 操作成功返回,和 start 规则刚好相反,主线程 A 等待子线程 B 完成,当子线程 B 完成后,主线程能够看到子线程 B 的赋值操作,将程序做个小改动,你也会秒懂的

public class JoinExample {        private int x = 0;        private int y = 1;        private boolean flag = false;        public static void main(String[] args) throws InterruptedException {                JoinExample joinExample = new JoinExample();                Thread thread1 = new Thread(joinExample::writer, "线程1");                thread1.start();                thread1.join();                System.out.println("x:" + joinExample.x );                System.out.println("y:" + joinExample.y );                System.out.println("flag:" + joinExample.flag );                System.out.println("主线程结束");        }        public void writer(){                this.x = 100;                this.y = 200;                this.flag = true;        }}

运行结果:

x:100y:200flag:true主线程结束Process finished with exit code 0

「主线程结束」这几个字打印出来喽,依旧和线程何时退出有关系

总结

  1. Happens-before 重点是解决前一个操作结果对后一个操作可见,相信到这里,你已经对 Happens-before 规则有所了解,这些规则解决了多线程编程的可见性与有序性问题,但还没有完全解决原子性问题(除了 synchronized)

  2. start 和 join 规则也是解决主线程与子线程通信的方式之一

  3. 从内存语义的角度来说, volatile 的写-读与锁的释放-获取有相同的内存效果;volatile 写和锁的释放有相同的内存语义; volatile 读与锁的获取有相同的内存语义,⚠️⚠️⚠️(敲黑板了) volatile 解决的是可见性问题,synchronized 解决的是原子性问题,这绝对不是一回事,后续文章也会说明

灵魂追问

  1. 同步块和同步方法在编译成 CPU 指令后有什么不同?

  2. 线程有 Daemon(守护线程)和非 Daemon 线程,你知道线程的退出策略吗?

  3. 关于 Happens-before 你还有哪些疑惑呢?

到此,关于"Happens-before的作用是什么"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!

线程 规则 代码 问题 程序 内存 结果 语义 编译 变量 就是 作用 有序 方法 有序性 处理 三个 关键 关键字 原则 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 科技金融是不是互联网金融 网络科技有限互联网 深圳互联网科技公司 期待互联网科技 凡科互联网科技 北京互联网络科技有限公司 互联网科技公司介绍 互联网科技大会案例 乌鲁木齐新华智原互联网科技学校 互联网科技和法律行业 南通创西澳互联网科技有限公司 中天科技中国工业互联网 宇信科技在杭州互联网排第几 互联网科技包装股份有限公司上市 广州燕处互联网科技有限公司 同城互联网络科技 悟空问答互联网科技 科技美学互联网 艺术 山东亿川互联网科技有限责任公司 互联网 科技节活动 微视频大赛 百单网互联网科技有限公司怎么样 赫美联合互联网科技 互联网 视角下的科技创新 互联网金融 科技创新 现代科技和互联网有关系吗 广州优美互联网科技 深圳国际航运科技和互联网 深圳市罗太互联网科技有限公司 保险科技服务互联网营销体系 广东猫王互联网科技有限公司电话
0