深入淺出Java中的Happens-Before核心規(guī)則
前言
在Java并發(fā)編程中,我們經(jīng)常會遇到這樣的問題:多線程環(huán)境下,一個線程對共享變量的修改,另一個線程能看到嗎?為什么有時候明明修改了變量,其他線程卻讀取到舊值?這些問題的答案,都與Java內(nèi)存模型(JMM)中的Happens-Before原則密切相關(guān)。
本文將從基礎(chǔ)概念出發(fā),循序漸進(jìn)地解析Happens-Before的定義、核心規(guī)則及實際應(yīng)用,幫助你徹底理解這一Java并發(fā)編程的基石。
一、Happens-Before是什么?為什么需要它?
1.1 從一個問題說起
先看一段簡單的代碼:
// 線程A執(zhí)行
int x = 1; // 操作A
boolean flag = true; // 操作B
// 線程B執(zhí)行
if (flag) { // 操作C
System.out.println(x); // 操作D
}如果線程A和線程B并發(fā)執(zhí)行,線程D會打印出1嗎?
直覺上應(yīng)該會,但實際上可能不會。因為在多線程環(huán)境中,編譯器優(yōu)化、CPU指令重排序、緩存等機(jī)制可能導(dǎo)致:
- 線程A中,操作A和B的執(zhí)行順序可能被調(diào)換(重排序)
- 線程A修改的x和flag可能還停留在CPU緩存中,未同步到主內(nèi)存
- 線程B可能讀取的是主內(nèi)存中未更新的舊值
這些問題會導(dǎo)致可見性和有序性問題。而Happens-Before原則就是JMM為解決這些問題提出的核心規(guī)范。
1.2 Happens-Before的定義
Happens-Before是JMM中定義的兩個操作之間的偏序關(guān)系:
如果操作A Happens-Before操作B,那么A的執(zhí)行結(jié)果必須對B可見,且A的執(zhí)行順序在邏輯上先于B。
注意:
- Happens-Before不代表“物理時間上的先后”,而是JMM定義的“邏輯先行”關(guān)系。
- 即使A在物理時間上后于B執(zhí)行,只要A Happens-Before B,A的結(jié)果就必須對B可見。
1.3 為什么需要Happens-Before?
JMM的核心目標(biāo)是:在保證并發(fā)程序正確性的前提下,盡可能為編譯器和CPU的優(yōu)化留出空間。
Happens-Before的價值在于:
- 它屏蔽了底層硬件和編譯器的復(fù)雜細(xì)節(jié)(如重排序、緩存等),為開發(fā)者提供了簡單清晰的可見性判斷標(biāo)準(zhǔn)。
- 它允許編譯器和CPU在不違反Happens-Before規(guī)則的前提下進(jìn)行優(yōu)化,保證性能。
簡單說:開發(fā)者只需關(guān)注是否滿足Happens-Before規(guī)則,無需關(guān)心底層如何實現(xiàn)可見性;JVM則需保證在滿足規(guī)則的情況下,底層優(yōu)化不破壞可見性。
二、Happens-Before的核心規(guī)則
JMM定義了8條核心的Happens-Before規(guī)則,這些規(guī)則是判斷可見性的基礎(chǔ)。我們逐一解析:
規(guī)則1:程序順序規(guī)則(Program Order Rule)
在同一個線程中,按照代碼順序,前面的操作Happens-Before后面的操作。
例如:
// 單線程內(nèi) int a = 1; // 操作A int b = a + 1; // 操作B
根據(jù)程序順序規(guī)則,A Happens-Before B。因此:
- 操作A的結(jié)果(a=1)對操作B可見
- 邏輯上A先于B執(zhí)行(即使編譯器可能重排序,但會保證結(jié)果等價于順序執(zhí)行)
規(guī)則2:監(jiān)視器鎖規(guī)則(Monitor Lock Rule)
對一個鎖的解鎖操作Happens-Before后續(xù)對同一個鎖的加鎖操作。
synchronized是Java中最典型的監(jiān)視器鎖,例如:
synchronized (lock) { // 加鎖
// 臨界區(qū)操作A
x = 1;
} // 解鎖(操作U)
// 其他線程
synchronized (lock) { // 加鎖(操作L)
// 臨界區(qū)操作B
System.out.println(x); // 必然打印1
}根據(jù)規(guī)則:
解鎖操作U Happens-Before 后續(xù)的加鎖操作L,因此操作A的結(jié)果(x=1)對操作B可見。
這就是synchronized能保證可見性的底層原因。
規(guī)則3:volatile變量規(guī)則(Volatile Variable Rule)
對volatile變量的寫操作Happens-Before后續(xù)對同一個volatile變量的讀操作。
例如:
// 線程A volatile int x = 0; x = 1; // 寫操作W // 線程B int y = x; // 讀操作R
根據(jù)規(guī)則:W Happens-Before R,因此線程B讀取到的y必然是1(而非0)。
原理:
volatile變量的寫操作會強(qiáng)制將緩存中的值刷新到主內(nèi)存,讀操作會強(qiáng)制從主內(nèi)存加載最新值,且禁止了volatile變量前后操作的重排序,從而保證可見性。
規(guī)則4:線程啟動規(guī)則(Thread Start Rule)
主線程對Thread對象的start()方法調(diào)用Happens-Before子線程中的所有操作。
例如:
// 主線程
int x = 1;
Thread t = new Thread(() -> {
// 子線程操作
System.out.println(x); // 必然打印1
});
t.start(); // 啟動操作S根據(jù)規(guī)則:S Happens-Before子線程中的打印操作,因此主線程在start()前對x的修改(x=1)對sub線程可見。
規(guī)則5:線程終止規(guī)則(Thread Termination Rule)
子線程中的所有操作Happens-Before主線程檢測到子線程終止。
主線程可以通過join()、isAlive()等方法檢測子線程是否終止,例如:
// 主線程
Thread t = new Thread(() -> {
// 子線程操作A
x = 1;
});
t.start();
t.join(); // 等待子線程終止(操作J)
System.out.println(x); // 必然打印1根據(jù)規(guī)則:子線程的操作A Happens-Before 主線程的操作J,因此主線程在join()后能看到x=1。
規(guī)則6:線程中斷規(guī)則(Thread Interruption Rule)
對線程interrupt()方法的調(diào)用Happens-Before被中斷線程檢測到中斷事件。
例如:
// 線程A
Thread t = new Thread(() -> {
// 子線程
if (Thread.interrupted()) { // 檢測中斷(操作C)
System.out.println("被中斷");
}
});
t.start();
t.interrupt(); // 中斷操作I
根據(jù)規(guī)則:I Happens-Before C,因此子線程能檢測到中斷事件。
規(guī)則7:傳遞性規(guī)則(Transitivity)
如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。
這是非常重要的一條規(guī)則,它可以將多個Happens-Before關(guān)系串聯(lián)起來,例如:
// 線程A
x = 1; // A
volatile boolean flag = true; // B
// 線程B
if (flag) { // C(讀volatile)
int y = x; // D
}- 根據(jù)程序順序規(guī)則:A Happens-Before B,C Happens-Before D
- 根據(jù)volatile規(guī)則:B Happens-Before C
- 根據(jù)傳遞性:A Happens-Before D → 因此D能看到x=1
規(guī)則8:對象終結(jié)規(guī)則(Finalizer Rule)
對象的構(gòu)造函數(shù)執(zhí)行完畢Happens-Before其finalize()方法開始執(zhí)行。
確保對象在被回收前,其構(gòu)造函數(shù)的所有初始化操作都已完成,例如:
class MyObject {
int x;
MyObject() {
x = 1; // 構(gòu)造函數(shù)操作A
}
@Override
protected void finalize() {
System.out.println(x); // 必然打印1(操作B)
}
}
根據(jù)規(guī)則:A Happens-Before B,因此finalize()中能看到構(gòu)造函數(shù)對x的初始化。
三、Happens-Before與重排序的關(guān)系
JMM允許編譯器和CPU進(jìn)行重排序優(yōu)化,但有一個前提:重排序不能破壞Happens-Before規(guī)則。
例如,在單線程中:
int a = 1; // A int b = 2; // B int c = a + b; // C
編譯器可能將A和B重排序(先執(zhí)行B再執(zhí)行A),但由于A和B之間沒有數(shù)據(jù)依賴,且重排序后C的結(jié)果仍為3,因此這種重排序是允許的——它沒有破壞程序順序規(guī)則的Happens-Before關(guān)系(A和B的執(zhí)行順序不影響最終結(jié)果的可見性)。
但如果是:
int a = 1; // A int b = a; // B
A和B存在數(shù)據(jù)依賴(B依賴A的結(jié)果),編譯器不能重排序A和B,否則會破壞程序順序規(guī)則的Happens-Before關(guān)系(B可能讀取到a的舊值)。
四、不滿足Happens-Before的情況:可見性問題
如果兩個操作之間不存在任何Happens-Before規(guī)則,JMM無法保證它們的可見性,可能出現(xiàn)“臟讀”。
例如:
// 線程A int x = 1; // A // 線程B System.out.println(x); // B
A和B之間沒有任何Happens-Before關(guān)系(不滿足上述8條規(guī)則中的任何一條),因此:
- 線程B可能打印1(x已同步到主內(nèi)存)
- 也可能打印0(x仍在A的CPU緩存中,未同步)
這種情況下,結(jié)果是不確定的,這就是多線程編程中“可見性問題”的根源。
五、Happens-Before的實際應(yīng)用
Happens-Before是理解Java并發(fā)工具的基礎(chǔ),以下是幾個典型場景:
5.1 synchronized與Happens-Before
synchronized通過“解鎖-加鎖”的Happens-Before關(guān)系保證可見性:
// 線程A
synchronized (lock) {
x = 1; // 解鎖前的操作
} // 解鎖U
// 線程B
synchronized (lock) { // 加鎖L(U Happens-Before L)
System.out.println(x); // 可見x=1
}5.2 volatile與Happens-Before
volatile通過“寫-讀”的Happens-Before關(guān)系保證可見性:
// 線程A
volatile boolean ready = false;
int data = 0;
data = 1; // A
ready = true; // B(寫volatile)
// 線程B
if (ready) { // C(讀volatile,B Happens-Before C)
System.out.println(data); // D(A Happens-Before D,因此可見1)
}這里結(jié)合了程序順序規(guī)則(A Happens-Before B)、volatile規(guī)則(B Happens-Before C)和傳遞性規(guī)則(A Happens-Before D)。
5.3 ConcurrentHashMap與Happens-Before
ConcurrentHashMap的put操作與get操作之間存在Happens-Before關(guān)系:
- 線程A的
put(k, v)操作Happens-Before線程B的get(k)操作 - 因此線程B的
get(k)能看到線程Aput的v值
這是ConcurrentHashMap保證線程安全的底層基礎(chǔ)之一。
六、常見面試問題
- Happens-Before的定義是什么?
- 它是JMM中定義的兩個操作之間的偏序關(guān)系:如果A Happens-Before B,則A的結(jié)果對B可見,且A的邏輯執(zhí)行順序先于B。
- Happens-Before和物理時間順序有什么區(qū)別?
- 無關(guān)。Happens-Before是邏輯先行關(guān)系,與物理時間上的先后無關(guān)。即使A在物理時間上后于B執(zhí)行,只要A Happens-Before B,A的結(jié)果就必須對B可見。
- volatile變量的寫操作和讀操作之間有什么Happens-Before關(guān)系?
- 對volatile變量的寫操作Happens-Before后續(xù)對該變量的讀操作,這保證了volatile變量的可見性。
- 如何利用Happens-Before規(guī)則判斷多線程操作的可見性?
- 只要兩個操作之間存在通過Happens-Before規(guī)則(直接或間接)建立的關(guān)系,就可以保證可見性;否則,可見性無法保證。
七、總結(jié)
Happens-Before是Java內(nèi)存模型的核心,它為開發(fā)者提供了判斷多線程操作可見性的清晰標(biāo)準(zhǔn)。理解Happens-Before,你就能:
- 明白為什么
synchronized、volatile等關(guān)鍵字能保證可見性 - 避免多線程編程中的“臟讀”“不可見”等問題
- 更深入地理解Java并發(fā)工具(如ConcurrentHashMap、AQS)的底層原理
記?。?strong>Happens-Before的本質(zhì)是“可見性契約”——JMM通過它承諾,只要滿足規(guī)則,就保證操作結(jié)果的可見性。掌握這一原則,是成為Java并發(fā)編程高手的關(guān)鍵一步。
到此這篇關(guān)于深入淺出Java中的Happens-Before核心規(guī)則的文章就介紹到這了,更多相關(guān)java Happens-Before內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring RestTemplate使用方法示例總結(jié)
這篇文章主要介紹了Spring RestTemplate使用方法示例總結(jié),本文通過實例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2025-04-04
基于springboot+vue實現(xiàn)垃圾分類管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了基于springboot+vue實現(xiàn)垃圾分類管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-07-07
MyBatis數(shù)據(jù)脫敏的實現(xiàn)方案介紹
在我們數(shù)據(jù)庫中有些時候會保存一些用戶的敏感信息,比如:手機(jī)號、銀行卡等信息,如果這些信息以明文的方式保存,那么是不安全的2022-08-08
Java?map和bean互轉(zhuǎn)常用的方法總結(jié)
這篇文章主要給大家介紹了關(guān)于Java中map和bean互轉(zhuǎn)常用方法的相關(guān)資料,平時日常Java開發(fā),經(jīng)常會涉及到Java?Bean和Map之間的類型轉(zhuǎn)換,需要的朋友可以參考下2023-09-09

