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

Happens-Before原则

第一部分:为什么需要Happens-Before?

1. 问题的根源:现代计算机的"记忆混乱症"

想象一下这个场景:你在厨房做饭,同时在看电视。

  • 单线程(顺序执行):先切菜 → 再炒菜 → 最后装盘。顺序很清晰。
  • 多线程(并发执行):你边切菜边看电视,还要接电话。大脑(CPU)可能会"记忆混乱":
    • 指令重排序:可能先接电话再切菜
    • 缓存不一致:眼睛看到的内容和耳朵听到的内容可能不同步

在计算机中,同样存在这些问题:

  • 编译器重排序:编译器为了优化性能,可能调整指令顺序
  • 处理器重排序:CPU可能乱序执行指令
  • 内存可见性:一个CPU核心修改了数据,其他核心可能看不到最新值

2. JMM(Java内存模型)的诞生

Java内存模型就是一套"交通规则",规定了:

  • 在什么情况下,一个线程的写操作对其他线程是可见的
  • 在什么情况下,操作之间必须保持顺序性

Happens-Before原则就是这套交通规则的核心!


第二部分:什么是Happens-Before?

1. 核心定义

Happens-Before 是一个关系,如果操作A Happens-Before 操作B,那么:

  1. A的结果对B可见
  2. A的执行顺序在B之前

2. 通俗比喻:公司的工作流程

假设公司有两个团队:开发团队测试团队

  • 没有Happens-Before

    • 开发可能还没写完代码,测试就开始测了
    • 开发修复了Bug,但测试不知道,还在测旧版本
  • 有Happens-Before

    • 开发完成代码 Happens-Before 测试开始
    • 开发提交代码 Happens-Before 测试拉取代码
    • 这样测试拿到的永远是最新代码,不会出现混乱

第三部分:八大Happens-Before规则详解

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

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

代码示例

int x = 1;          // 操作A
int y = x + 1;      // 操作B
System.out.println(y); // 操作C

解释

  • A Happens-Before B
  • B Happens-Before C
  • 所以B一定能看到A写入的x=1,C一定能看到B计算的y=2

注意:这只是逻辑上的顺序,实际执行时CPU可能会重排序,但保证最终结果与顺序执行一致。

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

定义:对一个锁的解锁Happens-Before于随后对这个锁的加锁。

代码示例

private final Object lock = new Object();
private int count = 0;// 线程A
public void incrementA() {synchronized(lock) {count = 1;          // 操作A} // 解锁               // 操作B(解锁)
}// 线程B  
public void readB() {synchronized(lock) {    // 操作C(加锁)System.out.println(count); // 操作D}
}

解释

  • 线程A的解锁(B) Happens-Before 线程B的加锁(C)
  • 所以线程B一定能看到线程A设置的count = 1

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

定义:对一个volatile变量的写操作Happens-Before于后面对这个变量的读操作。

代码示例

private volatile boolean flag = false;
private int data = 0;// 线程A
public void writer() {data = 42;          // 操作Aflag = true;        // 操作B(volatile写)
}// 线程B
public void reader() {if (flag) {         // 操作C(volatile读)System.out.println(data); // 操作D}
}

解释

  • B(volatile写) Happens-Before C(volatile读)
  • 由于程序顺序规则:A Happens-Before B
  • 根据传递性:A Happens-Before D
  • 所以线程B一定能看到data = 42

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

定义:Thread对象的start()方法调用Happens-Before于此线程的每一个动作。

代码示例

int config = 100;  // 操作AThread thread = new Thread(() -> {// 这个线程中的所有操作System.out.println(config); // 操作C
});config = 200;      // 操作B
thread.start();    // 操作D(启动线程)

解释

  • D(start) Happens-Before C(线程中的操作)
  • 由于程序顺序:A Happens-Before B Happens-Before D
  • 所以新线程看到的一定是config = 200

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

定义:线程中的所有操作都Happens-Before于其他线程检测到该线程已经终止。

代码示例

int result = 0;Thread worker = new Thread(() -> {result = 42;  // 操作A(工作线程中的操作)
});worker.start();   // 操作B
worker.join();    // 操作C(等待线程结束)
System.out.println(result); // 操作D

解释

  • A(工作线程的操作) Happens-Before C(join返回)
  • C Happens-Before D
  • 所以主线程一定能看到result = 42

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

定义:对线程interrupt()方法的调用Happens-Before于被中断线程检测到中断。

代码示例

Thread worker = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {// 工作}// 这里一定能看到中断状态
});worker.start();
// ... 一些操作
worker.interrupt(); // Happens-Before worker检测到中断

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

定义:一个对象的初始化完成(构造函数执行结束)Happens-Before于它的finalize()方法的开始。

规则8:传递性(Transitivity)

定义:如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。

这是最强大的规则,让我们可以串联多个Happens-Before关系。


第四部分:实战案例分析

案例1:双重检查锁定(Double-Checked Locking)

错误版本

public class Singleton {private static Singleton instance;public static Singleton getInstance() {if (instance == null) {                    // 第一次检查synchronized (Singleton.class) {if (instance == null) {            // 第二次检查instance = new Singleton();    // 问题在这里!}}}return instance;}
}

问题分析
instance = new Singleton() 包含三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将instance指向分配的内存

由于重排序,可能变成:1 → 3 → 2
这样其他线程可能在对象还没初始化完成时就拿到了引用!

正确版本(使用volatile)

public class Singleton {private static volatile Singleton instance;  // 添加volatilepublic static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();  // volatile写}}}return instance;  // volatile读}
}

Happens-Before分析

  • volatile写 Happens-Before volatile读
  • 所以其他线程一定能看到完全初始化的对象

案例2:计数器同步

需求:多个线程安全地增加计数器。

方案对比

// 方案1:使用synchronized(基于监视器锁规则)
class Counter {private int count = 0;public synchronized void increment() {count++;}public synchronized int getCount() {return count;}
}// 方案2:使用volatile(不适合,因为++不是原子操作)
class VolatileCounter {private volatile int count = 0;  // 错误!不能保证原子性public void increment() {count++;  // 非原子操作:读→改→写}
}// 方案3:使用AtomicInteger(基于volatile + CAS)
class AtomicCounter {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();  // 原子操作}
}

第五部分:内存屏障(Memory Barrier)的底层原理

Happens-Before在底层是通过内存屏障实现的。

四种内存屏障

  1. LoadLoad屏障

    Load1
    LoadLoad屏障
    Load2
    

    确保Load1的数据加载在Load2之前完成

  2. StoreStore屏障

    Store1
    StoreStore屏障  
    Store2
    

    确保Store1的数据对其他处理器可见在Store2之前

  3. LoadStore屏障

    Load1
    LoadStore屏障
    Store2
    

    确保Load1在Store2之前

  4. StoreLoad屏障

    Store1
    StoreLoad屏障
    Load2
    

    确保Store1对所有处理器可见在Load2之前(最强大,也最耗时)

Happens-Before与内存屏障的对应

  • volatile写:前面加StoreStore屏障,后面加StoreLoad屏障
  • volatile读:后面加LoadLoad屏障和LoadStore屏障
  • 锁的释放:相当于volatile写
  • 锁的获取:相当于volatile读

第六部分:总结与最佳实践

Happens-Before的核心价值

  1. 提供强保证:给程序员一个强内存模型,只要遵守规则,就能保证可见性和有序性
  2. 允许优化:给JVM和硬件足够的优化空间,在不违反规则的前提下可以重排序
  3. 简化编程:程序员不需要理解复杂的内存屏障和CPU架构

架构师建议

  1. 理解规则,但不滥用

    • 理解Happens-Before,但不要过度依赖它来推理复杂并发
    • 优先使用高级并发工具(java.util.concurrent包)
  2. 选择合适的同步机制

    • 简单同步 → synchronized
    • 状态标志 → volatile
    • 计数器等 → AtomicXXX
    • 复杂场景 → LockCountDownLatch
  3. 代码审查要点

    // ❌ 危险代码:没有Happens-Before关系
    boolean ready = false;  // 非volatile
    int data = 0;// 线程A
    data = 42;
    ready = true;// 线程B
    if (ready) {System.out.println(data); // 可能看到0!
    }// ✅ 安全代码:使用volatile建立Happens-Before
    volatile boolean ready = false;
    // 或者使用synchronized
    
  4. 调试技巧

    • 遇到诡异的并发bug时,用Happens-Before原则分析数据依赖关系
    • 检查共享变量的访问是否建立了正确的Happens-Before关系

最终记忆口诀

程序顺序保基础,锁的释放先于获取
volatile写先于读,线程启动先于运行
线程结束先于join,对象构造先于终结
传递关系串起来,并发安全有保障

掌握了Happens-Before原则,你就真正理解了Java并发内存模型的精髓,能够编写出正确、高效的并发程序!

http://www.dtcms.com/a/473498.html

相关文章:

  • 自己设置网站怎么做永远网站建设
  • 做网站的软件景宁县建设局网站
  • react多文件分片上传——支持拖拽与进度展示
  • Excel如何合并单元格?【图文详解】Excel合并单元格技巧?单元格合并高阶操作?
  • Fabric.js 完全指南:从入门到实战的Canvas绘图引擎详解
  • 学网站建设要多少钱遵义网站建设网站
  • 数据分析:Python懂车帝汽车数据分析可视化系统 爬虫(Django+Vue+销量分析 源码+文档)✅
  • 从Java集合到云原生现代数据管理的演进之路
  • 03_pod详解
  • 线性代数 | excellent algebraic space
  • 计算机网络篇之TCP滑动窗口
  • java项目使用宝塔面板部署服务器nginx不能反向代理找到图片资源
  • 180课时吃透Go语言游戏后端开发11:Go语言中的并发编程
  • 江苏建设部官方网站纯 flash 网站
  • Oracle OMF 配置文档
  • 帮别人做网站怎么赚钱wordpress 静态设置
  • SpringBoot Jar包冲突在线检测
  • 基于OpenCV的通过人脸对年龄、性别、表情与疲劳进行检测
  • vue3 类似 Word 修订模式,变更(插入、删除、修改)可以实时查看标记 如何实现
  • LLM 笔记 —— 07 Tokenizers(BPE、WordPeice、SentencePiece、Unigram)
  • Serverless数据库架构:FaunaDB+Vercel无缝集成方案
  • 【自然语言处理】“bert-base-chinese”的基本用法及实战案例
  • LLM 笔记 —— 08 Embeddings(One-hot、Word、Word2Vec、Glove、FastText)
  • 广告公司网站设计策划phpcmsv9手机网站
  • 【Qt】乌班图安装Qt环境
  • 边缘计算中的前后端数据同步:Serverless函数与Web Worker的异构处理
  • Windows Pad平板对 Qt 的支持
  • 基于JETSON ORIN/RK3588+AI相机:机器人-多路视觉边缘计算方案
  • 没有网怎么安装wordpress沈阳企业网站优化排名方案
  • 【C++STL :list类 (二) 】list vs vector:终极对决与迭代器深度解析 揭秘list迭代器的陷阱与精髓