Java 語言在設計之初就引入了線程的概念,以充分利用現代處理器的計算能力,這既帶來了強大、靈活的多線程機制,也帶來了線程安全等令人混淆的問題,而 Java 內存模型(Java Memory Model,JMM)為我們提供了一個在紛亂之中達成一致的指導準則。
今天我要問你的問題是,Java 內存模型中的 happen-before 是什么?
## 典型回答
Happen-before 關系,是 Java 內存模型中保證多線程操作可見性的機制,也是對早期語言規范中含糊的可見性概念的一個精確定義。
它的具體表現形式,包括但遠不止是我們直覺中的 synchronized、volatile、lock 操作順序等方面,例如:
* 線程內執行的每個操作,都保證 happen-before 后面的操作,這就保證了基本的程序順序規則,這是開發者在書寫程序時的基本約定。
* 對于 volatile 變量,對它的寫操作,保證 happen-before 在隨后對該變量的讀取操作。
* 對于一個鎖的解鎖操作,保證 happen-before 加鎖操作。
* 對象構建完成,保證 happen-before 于 finalizer 的開始動作。
* 甚至是類似線程內部操作的完成,保證 happen-before 其他 Thread.join() 的線程等。
這些 happen-before 關系是存在著傳遞性的,如果滿足 a happen-before b 和 b happen-before c,那么 a happen-before c 也成立。
前面我一直用 happen-before,而不是簡單說前后,是因為它不僅僅是對執行時間的保證,也包括對內存讀、寫操作順序的保證。僅僅是時鐘順序上的先后,并不能保證線程交互的可見性。
## 考點分析
今天的問題是一個常見的考察 Java 內存模型基本概念的問題,我前面給出的回答盡量選擇了和日常開發相關的規則。
JMM 是面試的熱點,可以看作是深入理解 Java 并發編程、編譯器和 JVM 內部機制的必要條件,但這同時也是個容易讓初學者無所適從的主題。對于學習 JMM,我有一些個人建議:
* 明確目的,克制住技術的誘惑。除非你是編譯器或者 JVM 工程師,否則我建議不要一頭扎進各種 CPU 體系結構,糾結于不同的緩存、流水線、執行單元等。這些東西雖然很酷,但其復雜性是超乎想象的,很可能會無謂增加學習難度,也未必有實踐價值。
* 克制住對“秘籍”的誘惑。有些時候,某些編程方式看起來能起到特定效果,但分不清是實現差異導致的“表現”,還是“規范”要求的行為,就不要依賴于這種“表現”去編程,盡量遵循語言規范進行,這樣我們的應用行為才能更加可靠、可預計。
在這一講中,兼顧面試和編程實踐,我會結合例子梳理下面兩點:
* 為什么需要 JMM,它試圖解決什么問題?
* JMM 是如何解決可見性等各種問題的?類似 volatile,體現在具體用例中有什么效果?
注意,專欄中 Java 內存模型就是特指 JSR-133 中重新定義的 JMM 規范。在特定的上下文里,也許會與 JVM(Java)內存結構等混淆,并不存在絕對的對錯,但一定要清楚面試官的本意,有的面試官也會特意考察是否清楚這兩種概念的區別。
## 知識擴展
**為什么需要 JMM,它試圖解決什么問題?**
Java 是最早嘗試提供內存模型的語言,這是簡化多線程編程、保證程序可移植性的一個飛躍。早期類似 C、C++ 等語言,并不存在內存模型的概念(C++ 11 中也引入了標準內存模型),其行為依賴于處理器本身的[內存一致性模型](https://en.wikipedia.org/wiki/Memory_ordering),但不同的處理器可能差異很大,所以一段 C++ 程序在處理器 A 上運行正常,并不能保證其在處理器 B 上也是一致的。
即使如此,最初的 Java 語言規范仍然是存在著缺陷的,當時的目標是,希望 Java 程序可以充分利用現代硬件的計算能力,同時保持“書寫一次,到處執行”的能力。
但是,顯然問題的復雜度被低估了,隨著 Java 被運行在越來越多的平臺上,人們發現,過于泛泛的內存模型定義,存在很多模棱兩可之處,對 synchronized 或 volatile 等,類似指令重排序時的行為,并沒有提供清晰規范。這里說的指令重排序,既可以是[編譯器優化行為](https://en.wikipedia.org/wiki/Instruction_scheduling),也可能是源自于現代處理器的[亂序執行](https://en.wikipedia.org/wiki/Out-of-order_execution)等。
換句話說:
* 既不能保證一些多線程程序的正確性,例如最著名的就是雙檢鎖(Double-Checked Locking,DCL)的失效問題,具體可以參考我在[第 14 講](http://time.geekbang.org/column/article/8624)對單例模式的說明,雙檢鎖可能導致未完整初始化的對象被訪問,理論上這叫并發編程中的安全發布(Safe Publication)失敗。
* 也不能保證同一段程序在不同的處理器架構上表現一致,例如有的處理器支持緩存一致性,有的不支持,各自都有自己的內存排序模型。
所以,Java 迫切需要一個完善的 JMM,能夠讓普通 Java 開發者和編譯器、JVM 工程師,能夠**清晰地**達成共識。換句話說,可以相對簡單并準確地判斷出,多線程程序什么樣的執行序列是符合規范的。
所以:
* 對于編譯器、JVM 開發者,關注點可能是如何使用類似[內存屏障](https://en.wikipedia.org/wiki/Memory_barrier)(Memory-Barrier)之類技術,保證執行結果符合 JMM 的推斷。
* 對于 Java 應用開發者,則可能更加關注 volatile、synchronized 等語義,如何利用類似 happen-before 的規則,寫出可靠的多線程應用,而不是利用一些“秘籍”去糊弄編譯器、JVM。
我畫了一個簡單的角色層次圖,不同工程師分工合作,其實所處的層面是有區別的。JMM 為 Java 工程師隔離了不同處理器內存排序的區別,這也是為什么我通常不建議過早深入處理器體系結構,某種意義上來說,這樣本就違背了 JMM 的初衷。

**JMM 是怎么解決可見性等問題的呢?**
在這里,我有必要簡要介紹一下典型的問題場景。
我在[第 25 講](http://time.geekbang.org/column/article/10192)里介紹了 JVM 內部的運行時數據區,但是真正程序執行,實際是要跑在具體的處理器內核上。你可以簡單理解為,把本地變量等數據從內存加載到緩存、寄存器,然后運算結束寫回主內存。你可以從下面示意圖,看這兩種模型的對應。

看上去很美好,但是當多線程共享變量時,情況就復雜了。試想,如果處理器對某個共享變量進行了修改,可能只是體現在該內核的緩存里,這是個本地狀態,而運行在其他內核上的線程,可能還是加載的舊狀態,這很可能導致一致性的問題。從理論上來說,多線程共享引入了復雜的數據依賴性,不管編譯器、處理器怎么做重排序,都必須尊重數據依賴性的要求,否則就打破了正確性!這就是 JMM 所要解決的問題。
JMM 內部的實現通常是依賴于所謂的內存屏障,通過禁止某些重排序的方式,提供內存可見性保證,也就是實現了各種 happen-before 規則。與此同時,更多復雜度在于,需要盡量確保各種編譯器、各種體系結構的處理器,都能夠提供一致的行為。
我以 volatile 為例,看看如何利用內存屏障實現 JMM 定義的可見性?
對于一個 volatile 變量:
* 對該變量的寫操作**之后**,編譯器會插入一個**寫屏障**。
* 對該變量的讀操作**之前**,編譯器會插入一個**讀屏障**。
內存屏障能夠在類似變量讀、寫操作之后,保證其他線程對 volatile 變量的修改對當前線程可見,或者本地修改對其他線程提供可見性。換句話說,線程寫入,寫屏障會通過類似強迫刷出處理器緩存的方式,讓其他線程能夠拿到最新數值。
如果你對更多內存屏障的細節感興趣,或者想了解不同體系結構的處理器模型,建議參考 JSR-133[相關文檔](http://gee.cs.oswego.edu/dl/jmm/cookbook.html),我個人認為這些都是和特定硬件相關的,內存屏障之類只是實現 JMM 規范的技術手段,并不是規范的要求。
**從應用開發者的角度,JMM 提供的可見性,體現在類似 volatile 上,具體行為是什么樣呢?**
我這里循序漸進的舉兩個例子。
首先,前幾天有同學問我一個問題,請看下面的代碼片段,希望達到的效果是,當 condition 被賦值為 false 時,線程 A 能夠從循環中退出。
~~~
// Thread A
while (condition) {
}
// Thread B
condition = false;
~~~
這里就需要 condition 被定義為 volatile 變量,不然其數值變化,往往并不能被線程 A 感知,進而無法退出。當然,也可以在 while 中,添加能夠直接或間接起到類似效果的代碼。
第二,我想舉 Brian Goetz 提供的一個經典用例,使用 volatile 作為守衛對象,實現某種程度上輕量級的同步,請看代碼片段:
~~~
Map configOptions;
char[] configText;
volatile boolean initialized = false;
// Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
// Thread B
while (!initialized)
sleep();
// use configOptions
~~~
JSR-133 重新定義的 JMM 模型,能夠保證線程 B 獲取的 configOptions 是更新后的數值。
也就是說 volatile 變量的可見性發生了增強,能夠起到守護其上下文的作用。線程 A 對 volatile 變量的賦值,會強制將該變量自己和當時其他變量的狀態都刷出緩存,為線程 B 提供可見性。當然,這也是以一定的性能開銷作為代價的,但畢竟帶來了更加簡單的多線程行為。
我們經常會說 volatile 比 synchronized 之類更加輕量,但輕量也僅僅是相對的,volatile 的讀、寫仍然要比普通的讀寫要開銷更大,所以如果你是在性能高度敏感的場景,除非你確定需要它的語義,不然慎用。
今天,我從 happen-before 關系開始,幫你理解了什么是 Java 內存模型。為了更方便理解,我作了簡化,從不同工程師的角色劃分等角度,闡述了問題的由來,以及 JMM 是如何通過類似內存屏障等技術實現的。最后,我以 volatile 為例,分析了可見性在多線程場景中的典型用例。
## 一課一練
關于今天我們討論的題目你做到心中有數了嗎?今天留給你的思考題是,給定一段代碼,如何驗證所有符合 JMM 執行可能?有什么工具可以輔助嗎?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?