沒有Happens-Before?你的多線程代碼就是‘一鍋粥’!
內存模型與happens-before:開發者與硬件的和平條約
在前文中,提到處理器通過一些特殊指令(如 LOCK、CMPXCHG、內存屏障等)來保障多線程環境下程序的正確性。然而,這種做法仍然存在幾個顯著問題。
1)底層指令實現復雜且晦澀:處理器指令的細節往往難以理解,開發者需要付出大量的時間和精力來掌握這些低級實現。
2)不同平臺間的兼容性問題:不同硬件架構和操作系統對這些指令的支持和實現方式各不相同,這要求程序在設計時考慮到跨平臺的兼容性和一致性。
3)多線程數據操作的復雜性:隨著程序業務邏輯的多變,處理器與線程之間的內存訪問依賴關系變得更加復雜,從而增加了程序出錯的風險。
為了簡化并發編程,解決這些問題,現代編程語言通常提供了抽象的內存模型,用以規范多線程環境下的內存訪問行為。這種抽象使開發者無需關注底層硬件與操作系統實現細節,即可編寫高效且可移植的并發程序。
以 Java 為例,Java語言采用了Java 內存模型(Java Memory Model,JMM)來提供這種抽象。 Java 內存模型的核心目的是通過支持諸如 volatile、synchronized、final 等同步原語,來確保在多線程環境下程序的原子性、可見性和有序性。這些原語確保了不同線程間的操作能夠按照特定的規則正確協作。
此外,JMM 還引入了一個重要概念:happens-before 關系,旨在描述并發編程中操作之間的偏序關系。具體來說,偏序關系主要用于確保線程間操作的順序性,避免因執行順序不明確而導致的并發問題。
偏序關系在并發編程中的應用主要體現在以下兩種情況。
1)程序順序(Program Order):指單線程中,由程序控制流決定的操作順序。例如,如果操作 A 在操作 B 之前執行,那么我們可以認為 A <= B。
2)同步順序(Synchronization Order):指由并發控制機制(如鎖、信號量等)所控制的操作順序。例如,如果操作 A 釋放了鎖,而操作 B 隨后獲取了該鎖,那么我們可以認為 A <= B。
除了 Java 之外,其他現代編程語言,如 Go、C++、Rust 等,也都實現了各自的 happens-before 關系機制,以確保并發程序的正確性和一致性。

Java內存模型的happens-before關系是用來描述兩個線程操作的內存可見性。需注意這里的可見性,并不代表A線程的執行順序一定早于B線程, 而是A線程對某個共享變量的操作對B線程可見。即A線程對變量a進行寫操作,B線程讀取到是變量a的變更值。
Java內存模型定義了主內存(Main memory),本地內存(Local memory),共享變量等抽象關系,來決定共享變量在多線程之間通信同步方式,即前面所說兩個線程操作的內存可見性。其中本地內存,涵蓋了緩存,寫緩沖區,寄存器以及其他硬件和編譯器優化等概念。

如圖所示,如果線程A與線程B之間要通信的話,必須要經歷下面2個步驟:
1)線程A把本地內存A中更新過的共享變量刷新到主內存中;
2)線程B到主內存中去讀取線程A之前已更新過的共享變量。
為了進一步抽象這種線程間的數據同步方式,Java內存模型定義了下述線程間的happens-before關系。
1)程序順序規則:單線程內(nei),每個操(cao)作(zuo)(zuo)happens-before于(yu)該(gai)線程中的任意(yi)后續操(cao)作(zuo)(zuo)。
// Thread1內, A happens-before B,B happens-before C。
// 這意味著A一定會在B之前完成,B一定會在C之前完成。因此,可以確信y包含x+5的結果。
Thread1 {
x = 10; // A
y = x + 5; // B
print(y); // C
}
2)監視器鎖(suo)(suo)規則(ze):釋放(fang)鎖(suo)(suo)的操作happens-before之后對同(tong)一把鎖(suo)(suo)的獲(huo)取的鎖(suo)(suo)操作。
// Thread1釋放鎖(B)happens-before Thread2獲取鎖(C)。
// 這意味著當Thread2打印x時,它看到的一定是"Thread1 data",因為Thread1的修改操作和釋放鎖操作,都在Thread2獲取鎖之前完成。
lock = Lock()
Thread1 {
lock.acquire()
x = "Thread1 data" // A
lock.release() // B
}
Thread2 {
lock.acquire() // C
print(x) // D
lock.release()
}
3)volatile變量規則:volatile字段的寫操作(zuo)happens-before之后對同一字段的讀操作(zuo)。
// 對volatile字段的寫操作(A)happens-before之后對同一字段的讀操作(B)。
// 這意味著當Thread2讀取x時,它看到的一定是100,因為Thread1的寫操作在Thread2的讀操作之前完成。
volatile int sharedData; // 聲明一個volatile變量
Thread1 {
x = 100; // A
}
Thread2 {
int localData = x; // B
print(localData);
}
4)傳遞(di)性規則:如果(guo)A happens-before B,且B happens-before C,那么A happens-before C。
// 如果Thread1 A happens-before Thread2 B,且Thread2 B happens-before Thread2 C,那么Thread1 A happens-before Thread2 C。
// 這意味著A一定會在C之前完成。因此,可以確信z包含(x+5)*2的結果,因為賦值給x的操作和計算x+5的操作都在計算y*2的操作之前完成。
Thread1 {
x = 10; // A
}
Thread2 {
y = x + 5; // B
z = y * 2; // C
print(z);
}
5)start()規(gui)則:如果線程A執行操(cao)作(zuo)ThreadB.start(),那么(me)A線程的ThreadB.start()操(cao)作(zuo)happens-before于線程B中(zhong)的任意操(cao)作(zuo)。
// 如果Thread1執行操作ThreadB.start(),那么Thread1 A happens-before Thread2 C。
// 這意味著A一定會在C之前完成。因此,可以確信Thread1 x值為10,因為賦值給x的操作在打印x的操作之前完成。
Thread1 {
ThreadB.start(); // A
x = 10; // B
}
Thread2 {
print(x); // C
}
6)join()規則:如果(guo)線(xian)(xian)程A執行操作ThreadB.join()并成(cheng)功返(fan)回(hui)(hui),那(nei)么線(xian)(xian)程B中的任意操作 happens-before 線(xian)(xian)程A從ThreadB.join()操作成(cheng)功返(fan)回(hui)(hui)。
// 如果線Thread1執行操作Thread2.join(),那么Thread2 D happens-before Thread1 C。
// 這意味著D一定會在C之前完成。因此,可以確信Thread1 x值為10,因為賦值給x的操作在打印x的操作之前完成。
Thread1 {
Thread2.start(); // A
Thread2.join(); // B
print(x); // C
}
Thread2 {
x = 10; // D
}
如上的happens-before關系中(zhong)(zhong),與日常開發(fa)密切相關的是1、2、3、4四(si)個規(gui)則(ze)。其(qi)中(zhong)(zhong)規(gui)則(ze)1滿足了as-if-serial語義(yi),即Java內存模型允許代碼(ma)和(he)指令重排(pai)序,只(zhi)要不(bu)影響程(cheng)序執行結(jie)果(guo)。規(gui)則(ze)2和(he)3是通過synchronized、volatile關鍵字實現。結(jie)合規(gui)則(ze)1、2、3來(lai)看看規(gui)則(ze)4的具體使用,可以看到(dao)如下的代碼(ma),程(cheng)序最終執行且得到(dao)正確結(jie)果(guo)。
// x, y, z被volatile關鍵字修飾
private volatile int x, y, z;
public void test() {
Thread a = new Thread(() -> {
// 基于程序順序規則
// 沒有數據依賴關系,可以重排序下面代碼
int i = 0;
x = 2;
// 基于volatile變量規則
// 編譯器插入storeload內存屏障指令
// 1)禁止代碼和指令重排序
// 2)強制刷新變量x的最新值到內存
});
Thread b = new Thread(() -> {
int i = 0;
// 存在數據依賴關系,無法重排序下面代碼
// 強制從主內存中讀取變量x的最新值
y = x;
// 基于volatile變量規則
// 編譯器插入storeload內存屏障指令
// 1)禁止代碼和指令重排序
// 2)強制刷新變量y的最新值到內存
// 3)y = x;可能會被編譯優化去除
y = 3;
// 編譯器插入storeload內存屏障指令
// 1)禁止代碼和指令重排序
// 2)強制刷新變量y的最新值到內存
});
Thread c = new Thread(() -> {
// 基于程序順序規則
// 沒有數據依賴關系,可以重排序下面代碼
int i = 0;
// 基于volatile變量規則
// 強制從主內存中讀取變量x和y的最新值
z = x * y;
// 編譯器插入storeload內存屏障指令
// 1)禁止代碼和指令重排序
// 2)強制刷新變量z的最新值到內存
});
// ...start啟動線程,join等待線程
assert z == 6;
// 可以看到a線程對變量x變更,b線程對變量y變更,最終對線程c可見
// 即滿足傳遞性規則
}
private int x, y, z;
public void test() {
Thread a = new Thread(() -> {
// synchronized,同步原語,程序邏輯將順序串行執行
synchronized (this){
// 基于程序順序規則
// 沒有數據依賴關系,可以重排序下面代碼
int i = 0;
x = 2;
// 基于監視器鎖規則
// 強制刷新變量x的最新值到內存
}
});
Thread b = new Thread(() -> {
// synchronized,同步原語,程序邏輯將順序串行執行
synchronized (this) {
int i = 0;
// 存在數據依賴關系,無法重排序下面代碼
// 強制從主內存中讀取變量x的最新值
y = x;
// 基于監視器鎖規則
// 1)強制刷新變量y的最新值到內存
// 2)y = x;可能會被編譯優化去除
y = 3;
// 強制刷新變量y的最新值到內存
}
});
Thread c = new Thread(() -> {
// synchronized,同步原語,程序邏輯將順序串行執行
synchronized (this) {
// 基于程序順序規則
// 沒有數據依賴關系,可以重排序下面代碼
int i = 0;
// 基于監視器鎖規則
// 強制從主內存中讀取變量x和y的最新值
z = x * y;
// 強制刷新變量z的最新值到內存
}
});
// ...start啟動線程,join等待線程
assert z == 6;
// 可以看到a線程對變量x變更,b線程對變量y變更,最終對線程c可見
// 即滿足傳遞性規則
}
總結:在混沌與秩序間搭建橋梁
Java內存模型是并發編程中連接開發者與硬件系統的關鍵橋梁。它依托可見性、有序性和原子性這三大核心原則,將復雜的并發問題轉化為清晰的編程規范。當多個線程操作共享變量時,Java內存模型利用volatile、synchronized等機制,有效抑制了處理器優化帶來的不確定性,同時兼顧了性能優化需求。其定義的happens-before關系,如同線程間的通信準則,以順序性規則替代了對緩存刷新、指令重排等底層操作的直接操控。這種設計讓開發者能夠專注于業務邏輯,僅憑有限的同步手段就能構建出穩健的多線程程序。
Java內存模型的價值(zhi)在于達成了三個(ge)重要平衡:它確保程序正確性(xing)不依賴于硬件實現細(xi)節;維持(chi)同步規則的簡(jian)潔性(xing)以(yi)控(kong)制復雜度;讓開發者能以(yi)較低的認知成本(ben)構建并(bing)發系統。這無疑是工程解耦的典范:用簡(jian)潔的抽象(xiang)來掌控(kong)復雜的世界。
很高興與你相遇!
如果你喜歡本文內容,記得關注哦!!!
本文來自博客園,作者:poemyang,轉載請注明原文鏈接://www.xtjzw.net/poemyang/p/19012883