happen-before原则

什么是 happen-before 原则?

happen-before 是一个逻辑关系,用于描述两个操作之间的 “先后顺序”—— 如果操作 A happen-before 操作 B,那么 A 的执行结果必须对 B 可见,且 A 的执行顺序在逻辑上先于 B。也就是保证指令有序性和共享变量的可见性。

具体的 happen-before 规则

JMM 定义了 9 条核心 happen-before 规则,每条规则都直接或间接关联可见性:

规则名称描述代码示例可见性体现与说明
程序次序规则在同一个线程中,按照程序的控制流顺序,前面的操作 Happens-Before 于后面的任何操作。int a = 1;
int b = a; // b 一定能看到 a=1
线程内,后面的操作一定能看到前面操作对变量的修改。这是单线程语义的基础。
管程锁定规则对一个锁的解锁操作 Happens-Before 于后续对同一个锁的加锁操作。// 线程A
synchronized(lock) {
sharedVar = 100;
} // 解锁

// 线程B
synchronized(lock) { // 加锁
print(sharedVar); // 保证看到100
}
线程B在获得锁之后,一定能看到线程A在释放同一把锁之前对所有共享变量所做的修改。管程(Monitor) 指锁,synchronizedLock 实现类(如 ReentrantLock)都遵守此规则。
volatile 变量规则对一个 volatile 变量的写操作 Happens-Before 于后续任何一个对该变量的读操作。// 线程A
sharedData = ...; // 普通写
volatileFlag = true; // volatile写

// 线程B
if (volatileFlag) { // volatile读
print(sharedData); // 能看到sharedData的修改
}
线程B读到 volatileFlagtrue 时,不仅能看到 volatileFlag 的最新值,也能看到线程A在写 volatileFlag 之前的所有写操作。
线程启动规则主线程调用子线程的 start() 方法 Happens-Before 于该子线程中的任何操作。int x = 10; // 启动前修改
Thread t = new Thread(() -> {
int finalX = x; // 子线程读取
System.out.println(finalX); // 输出10
});
// x = 20; // 此处赋值会导致编译错误!
t.start();
子线程开始执行时,能看到主线程在调用 start() 之前对(effectively final的)变量x的修改。注意:由于Lambda与匿名内部类要求局部变量是final或effectively final的,主线程无法在创建线程后再修改x。 如果x是成员变量,那么修改x = 20,子线程可以读取到20
线程终止规则一个线程中的所有操作都 Happens-Before 于其他线程成功从该线程的 join() 方法返回。// 子线程
Thread t = new Thread(() -> {
result = compute(); // 子线程中计算
});
t.start();
t.join(); // 等待子线程终止
System.out.println(result); // 能看到result的修改
主线程在 join() 成功返回后,能 guaranteed 看到子线程在执行过程中对共享变量(如result)的所有修改。
线程中断规则调用线程 interrupt() 方法 Happens-Before 于被中断线程检测到中断状态。// 线程A
threadB.interrupt(); // 中断操作

// 线程B
if (Thread.interrupted()) {
// 一定能感知到中断操作
}
如果一个线程被中断,它之后检测中断状态时,一定能看到那个中断请求。
对象终结规则一个对象的构造函数执行结束 Happens-Before 于它的 finalize() 方法的开始。public class MyClass {
private int value;
MyClass() {
value = 50; // 构造器内初始化
} // 构造结束

protected void finalize() {
// 此处一定能看到 value == 50
}
}
保证垃圾回收器在回收对象之前,该对象已经被完全正确地初始化了。
传递性如果操作 A Happens-Before B,且操作 B Happens-Before C,那么可以得出操作 A Happens-Before C。// 线程A
sharedVar = 1; // (A) 普通写
volatileFlag = true; // (B) volatile写

// 线程C
if (volatileFlag) { // (C) volatile读 (B hb C)
// 根据传递性: A hb B, B hb C, 所以 A hb C
// 故此处能看到 A 的写入结果 (sharedVar=1)
}
该规则是连接其他规则的桥梁,使得跨线程的可见性保证能够通过中间操作进行传递。
final 字段规则对于一个包含 final 字段的对象,其构造函数的结束 Happens-Before 于任何其他线程获取到该对象引用并访问其 final 字段。public class FinalExample {
private final int x = 42; // final字段
}

// 其他线程
FinalExample obj = ...; // 获取对象引用
System.out.println(obj.x); // 保证看到42
其他线程在拿到一个包含final字段的对象引用后,无须额外的同步,就能 guaranteed 看到 final 字段被构造器初始化的值。

补充说明第四条规则中局部变量与成员变量在匿名内部类中的访问区别

生命周期不匹配:

局部变量 x 存储在栈内存中,其生命周期与 方法的执行周期相同
匿名内部类对象(task)存储在堆内存中,其生命周期可能比 方法更长
如果允许内部类访问非 final 的局部变量,当 方法执行完毕,x 的栈帧被销毁后,内部类对象可能还在运行,这将导致访问无效内存

成员变量 x 存储在堆内存中,与匿名内部类对象具有相同的生命周期
内部类通过隐式持有外部类的引用(RunnableExample.this)来访问成员变量

值捕获机制:

Java 通过值捕获来解决这个问题:在创建内部类实例时,将局部变量的值复制一份到内部类中
为了保证复制值与原始变量的一致性,Java 要求局部变量必须是 final 或 effectively final
这样内部类使用的就是捕获时的值快照,不会受到外部修改的影响

内部类不是捕获成员变量的值,而是通过引用访问它
因此,对成员变量的修改会反映到内部类中

总结对比

特性局部变量成员变量
存储位置栈内存堆内存
生命周期与方法调用相同与对象实例相同
内部类访问方式值捕获(复制)引用访问
final 要求必须为 final 或 effectively final无要求
修改可见性内部类看不到外部修改内部类可以看到外部修改
线程安全性由语言机制保证需要开发者自己保证

synchronized 关键字

最基础的内置锁,通过同步代码块或同步方法实现:
进入 synchronized 块(加锁)时,线程会清空本地缓存,从主内存加载共享变量的最新值。
退出 synchronized 块(解锁)时,线程会将本地缓存中修改的共享变量刷新到主内存。
示例:

private int count = 0;// 同步方法
public synchronized void increment() {count++; // 解锁时会将修改刷新到主内存
}// 同步代码块
public void getCount() {synchronized (this) {return count; // 加锁时会从主内存加载最新值}
}

java.util.concurrent.locks.Lock 接口的实现类

显式锁,最常用的实现是 ReentrantLock,还包括 ReentrantReadWriteLock 等:
调用 lock() 方法(加锁)时,线程会失效本地缓存,强制从主内存加载变量。
调用 unlock() 方法(解锁)时,线程会将本地缓存中的修改刷新到主内存。
示例(ReentrantLock):

private final Lock lock = new ReentrantLock();
private int count = 0;public void increment() {lock.lock();try {count++; // 解锁时刷新到主内存} finally {lock.unlock();}
}public int getCount() {lock.lock();try {return count; // 加锁时从主内存加载} finally {lock.unlock();}
}

读写锁 ReentrantReadWriteLock
分离读锁和写锁,更细粒度的控制:
写锁(writeLock()):获取时会强制加载最新值,释放时会刷新修改到主内存(同普通锁)。
读锁(readLock()):多个线程可同时获取,能看到之前写锁释放的所有修改(保证读操作可见性)。

著名的双重检查单例模式

public class Singleton {// 关键1:使用volatile修饰单例实例private static volatile Singleton instance;// 关键2:私有构造函数,防止外部直接实例化private Singleton() {// 初始化逻辑}// 关键3:双重检查锁定获取实例public static Singleton getInstance() {// 第一次检查:避免不必要的同步(提高性能)if (instance == null) {// 关键4:同步块,保证多线程安全synchronized (Singleton.class) {// 第二次检查:防止多线程同时进入同步块后重复创建实例if (instance == null) {// 关键5:创建实例(volatile在此处防止指令重排序)instance = new Singleton();}}}return instance;}
}

关键代码解析

volatile 修饰符的作用
volatile 在这里有两个核心作用:
保证 instance 变量的可见性(多线程环境下,一个线程对 instance 的修改会立即被其他线程感知),因为第一次检查并使用synchronized 关键字将instance 包含在内,所以必须使用volatile关键字保证可见性。
禁止指令重排序(这是 DCL 模式中 volatile 的核心价值)。
双重检查的意义
第一次检查(同步块外):避免每次调用 getInstance() 都进入同步块,提高性能(多数情况下 instance 已初始化,无需同步)。
第二次检查(同步块内):防止多个线程同时通过第一次检查后,在同步块内重复创建实例。

volatile 如何禁止指令重排序?

对象创建过程(instance = new Singleton())在 JVM 中会被拆分为三步操作:

1. memory = allocate();       // 分配内存空间
2. ctorInstance(memory);      // 初始化对象(执行构造函数)
3. instance = memory;         // 将引用指向内存地址

问题场景:

如果没有 volatile 修饰,编译器或 CPU 可能对步骤 2 和 3 进行重排序,导致执行顺序变为:1 → 3 → 2。
此时会出现严重问题:

线程 A 执行到步骤 3 后,instance 已非 null(引用已指向内存),但步骤 2 尚未完成(对象未初始化)。
线程 B 此时进行第一次检查(instance == null),会发现 instance 不为 null,直接返回一个未初始化完成的对象,导致程序异常。

volatile 的解决方案:

volatile 通过在对象创建指令前后插入内存屏障(Memory Barrier) 禁止这种重排序:

在步骤 3 之后插入 StoreStore 屏障:禁止初始化对象(步骤 2)与设置引用(步骤 3)的重排序。
在步骤 3 之后插入 StoreLoad 屏障:确保引用赋值(步骤 3)完成后,才允许其他线程读取 instance。

这两个内存屏障强制保证了执行顺序为 1 → 2 → 3,即对象完全初始化后,才会将引用赋值给 instance,从而避免线程 B 读取到未初始化的对象。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/921477.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/921477.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

4.1 机器学习 - 评估指标

模型评估是判断 “模型是否有效” 的核心环节,需结合任务类型(分类 / 回归)、数据分布(如类别不平衡)和商业目标选择指标。本节聚焦分类任务的核心评估指标,从定义、计算逻辑到适用场景逐一拆解&#xff0c…

雅菲奥朗SRE知识墙分享(七):『可观测性的定义与实践』

在分布式系统日益复杂的当下,故障不再是“是否发生”,而是“何时爆发”。SRE可观测性正是应对不确定性的“显微镜”与“导航仪”:通过指标、日志、追踪三大数据血脉,实时外化系统黑盒,让每一次抖动、每一行报错、每一次…

C++ 详细讲解vector类

目录 1. 什么是vector? 2. vector的使用 1. 构造函数---初始化 1. 默认构造函数(无参构造) 2. 填充构造函数(指定数量和初始值) 3. 范围构造函数(通过迭代器拷贝其他容器元素) 4. 拷贝构造函数(直接拷贝另一个vector) 注…

Windows Server2012 R2 安装.NET Framework 3.5

Windows Server2012 R2 安装.NET Framework 3.5 虚拟机系统是Windowsserver 2012R2,在安装SQlserver2012时候警告未安装.NET Framework 3.5。于是找了个.NET Framework 3.5的安装包,但是由于系统原因无法正常安装。按照提示从控制面板-程序-启动或关闭Wi…

IDEA中Transaction翻译插件无法使用,重新配置Transaction插件方法

原因 由于Transaction默认的翻译引擎为谷歌翻译,由于一些原因,这个翻译无法使用,因此导致插件无法使用。 解决办法 更换Transaction插件翻译引擎即可。 方法步骤 1.进入Idea的设置里,找到Tool下的Transaction选项2.更改翻译引擎&a…

外置flash提示音打包脚本

批处理脚本说明文档 - 音频资源打包与分发 一、脚本功能概述 本批处理脚本(.bat 文件)用于将指定目录下的多个音频文件(.wtg 和 .mp3 格式)打包为音频资源配置文件(tone.cfg),进一步将配置文件与…

Go语言设计模式(三)抽象工厂模式

抽象工厂模式与工厂模式类似,被认为是工厂方法模式的另一层抽象.抽象工厂模式围绕创建其他工厂的超级工厂工作.1.角色:1.1抽象产品:构成产品系列的一组不同但相关的产品的声明接口.1.2具体产品:实现抽象产品接口的类,主要用于定义产品对象,由相应的具体工厂创建.1.3抽象工厂:创…

大狗王 DG1+ 13.6G矿机详细参数解析与性能评测

近年来,随着加密货币挖矿行业的不断发展,越来越多的矿机厂商推出了高性能、低功耗的矿机设备。大狗王(DG1)13.6G矿机便是其中一款备受关注的设备,特别是在LTC(莱特币)、Doge(狗狗币&…

Python 算术运算练习题

计算数字特征值题目描述 编写一个程序,接收用户输入的两个整数 a 和 b(a > b > 0),计算并输出以下结果:a 与 b 的和的平方a 除以 b 的商和余数a 与 b 的平均数(保留 2 位小数)示例请输入整…

OS项目构建效能改进策划方案

一、现状分析与问题定位构建稳定性问题: 表现:非代码变更引发的构建失败(如环境依赖、工具链版本、第三方库更新、资源竞争等)“幽灵构建”时有发生。影响:严重破坏开发流程的顺畅性,耗费大量开发/测试人员…

Ai8051 2.4寸320*240 ILI9341 I8080接口驱动

/*---------------------------------------------------------------------*/ /* --- Web: www.STCAI.com ---------------------------------------------*/ /* 液晶屏驱动程序参考wiki技术网站提供的开源源码,仅供学习使用 */ /*----------------------…

最大似然估计:损失函数的底层数学原理

引言当你第一次看到线性回归时,你是否注意到了作为参数优化关键的损失函数(均方损失),你是否能够理解它的本质和由来。其实,在我第一次接触时,我是感到有些惊讶的,然后试着去强行理解它&#xf…

使用 n8n 结合通义千问大模型构建业务数据库分析智能体

一、项目概述 本项目致力于构建一个结合 n8n 工作流引擎 与 通义千问大模型 的智能体,旨在对庞大的业务数据库进行自动化分析、语义建模及自然语言问答。通过不同工作流的迭代构建,实现了表结构解析、业务含义提取、关系可视化、问答服务等能力&#xff…

css margin外边距重叠/塌陷问题

一、定义 相邻块级元素或父子元素的垂直外边距会合并&#xff08;折叠&#xff09;为单个边距&#xff0c;其大小为单个边距的最大值&#xff08;或如果他们相等&#xff0c;则仅为其中的一个&#xff09;&#xff0c;这种行为称为边距折叠 <div style"margin-bottom: …

可重复读 是否“100%”地解决幻读?

这是一个非常深刻的问题&#xff0c;答案是&#xff1a;几乎解决了&#xff0c;但在一个非常特殊且罕见的边界场景下&#xff0c;理论上仍然可能出现幻读。 因此&#xff0c;严格来说&#xff0c;它并非被“彻底”或“100%”地解决。下面我们来详细分解这个结论&#xff1a;1. …

从零开始的云计算生活——第五十八天,全力以赴,Jenkins部署

目录 一.故事背景 二.安装Jenkins必要插件 1.安装Publish Over SSH 2.安装maven integration插件 3. 配置jenkins并发执行数量 4. 配置邮件地址 三. 基于Jenkins部署PHP环境 1. 下载ansible插件 2. 下载ansible应用 3. 构建项目 ​编辑 使用Jenkins账户生成ssh密钥 …

串口HAL库发送问题

想了很久&#xff0c;不知道该标题起的是否合适&#xff0c;该篇Blog用于记录在使用HAL库的USART模块时实际遇到的一个涉及发送方式的问题&#xff0c;用于提醒自身同时也希望能帮到各位。程序问题叙述先来看一段代码&#xff1a;void CusUSART_SendByte_IT( uint8_t Byte ) { …

CUDA默认流的同步行为

默认流 对于需要指定 cudaStream_t参数的 cuda API&#xff0c;如果将 0作为实参传入&#xff0c;则视为使用默认流&#xff1b;对于不需要指定 cudaStream_t参数的 cuda API&#xff0c;则也视为使用默认流。 在 cuda中&#xff0c;默认流有两种类型&#xff0c;一种是 legacy…

「数据获取」《中国电力统计年鉴》(1993-2024)(含中国电力年鉴)

01、数据简介一、《中国电力统计年鉴》作为全面系统反映中国电力领域发展状况的权威性年度统计资料&#xff0c;涵盖了电力建设、生产、消费及供需等全链条关键信息。其编制工作有着深厚的历史积淀&#xff0c;可追溯至 20 世纪 50 年代&#xff0c;历经数十年的积累与完善&…

《AI大模型应知应会100篇》第68篇:移动应用中的大模型功能开发 —— 用 React Native 打造你的语音笔记摘要 App

&#x1f4f1; 第68篇&#xff1a;移动应用中的大模型功能开发 —— 用 React Native 打造你的语音笔记摘要 App &#x1f3af; 核心目标&#xff1a;零门槛集成大模型&#xff0c;5步开发跨平台智能功能 &#x1f9e9; 适用人群&#xff1a;前端开发者、产品经理、独立开发者 …