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
英文地址,請注明出處:http://en.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;前端開發者、產品經理、獨立開發者 …