Java 線程之間的通信對程序員完全透明岖是,內(nèi)存可見性問題很容易困擾 Java 程序員栏赴,本文將簡要介紹 Java 內(nèi)存模型。
Java 內(nèi)存模型的基礎(chǔ)
并發(fā)編程模型的兩個關(guān)鍵問題
在并發(fā)編程中毯炮,需要處理兩個關(guān)鍵問題:線程之間如何通信及線程之間如何同步省咨。通信是指線程之間以何種機制來交換信息肃弟。在命令式編程中,線程之間的通信機制有倆種:共享內(nèi)存和消息傳遞茸炒。
在共享內(nèi)存的并發(fā)模型里愕乎,線程之間共享程序的公共狀態(tài),通過寫-讀內(nèi)存中的公共狀態(tài)進行隱式通信壁公。在消息傳遞的并發(fā)模型里感论,線程之間沒有公共狀態(tài),線程之間必須通過發(fā)送消息來顯示通信紊册。
同步是指程序中用于控制不同線程間操作發(fā)生相對順序的機制比肄。在共享內(nèi)存并發(fā)模型里快耿,同步是顯示進行的。程序員必須顯示指定某個方法或某段代碼需要在線程之間互斥執(zhí)行芳绩。在消息傳遞的并發(fā)模型里掀亥,由于消息的發(fā)送必須在消息的接受之前,因此同步是隱式進行的妥色。
Java 的并發(fā)采用的是共享內(nèi)存模型搪花,Java 線程之間的通信是隱式進行的,整個通信過程對程序員完全透明嘹害,但是線程之間同步是顯式進行的撮竿,需要程序員顯式的進行線程同步。
Java 內(nèi)存模型的抽象結(jié)構(gòu)
在 Java 中笔呀,所有實例域幢踏、靜態(tài)域和數(shù)組元素都存儲在堆內(nèi)存中,堆內(nèi)存在線程之間共享许师。局部變量房蝉、方法定義參數(shù)和異常處理器參數(shù)不會在線程之間共享,它們不會有線程可見性問題微渠,也不受內(nèi)存模型的影響搭幻。
Java 線程之間的通信由 Java 內(nèi)存模型(JMM)控制, JMM 決定一個線程對共享變量的寫入何時對另一個線程可見敛助。從抽象的角度來看粗卜, JMM 定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存中屋确,每個線程都有一個私有本地內(nèi)存纳击,本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是 JMM 的一個抽象概念攻臀,并不真實存在焕数,它涵蓋了緩存、寫緩沖區(qū)刨啸、寄存器以及其他的硬件和編譯器優(yōu)化堡赔。Java 內(nèi)存模型的抽象示意圖如下:
從上圖來看,如果線程 A 與線程 B 之間要通信的話设联,必須要經(jīng)過下面兩個步驟善已。
1)線程 A 把本地內(nèi)存 A 中更新過的共享變量刷新到主存中去。
2)線程 B 到主存中去讀線程 A 之前已經(jīng)更新過的共享變量离例。
從源代碼到指令序列的重排序
在執(zhí)行程序時换团,為了提高性能,編譯器和處理器常常會對指令做重排序宫蛆。重排序分為三種類型艘包。
- 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序想虎。
- 指令級并行的重排序∝宰穑現(xiàn)代處理器采用了指令級并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性舌厨,處理器可以改變語句對應(yīng)機器指令的執(zhí)行順序岂却。
- 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū)裙椭,著使得加載和存儲操作看上去可能是在亂序執(zhí)行淌友。
上述的 1 屬于編譯器重排序,上述的 2 和 3 屬于處理器重排序骇陈。這些重排序可能會導致多線程出現(xiàn)內(nèi)存可見性問題震庭。對于編譯器, JMM 的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序你雌。對于處理器重排序器联, JMM 的處理器重排序規(guī)則會要求 Java 編譯器在生成指令序列時,插入特定類型的內(nèi)存屏障指令婿崭,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序拨拓。
JMM 屬于語言級的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺之上氓栈,通過禁止特定類型的編譯器重排序和處理器重排序渣磷,為程序員提供一致的內(nèi)存可見性保證。
happens-before 簡介
JMM 中使用 happens-before 的概念來闡述操作之間的內(nèi)存可見性授瘦。在 JMM 中醋界,如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,那么這兩個操作之間必須要存在 happens-before 關(guān)系提完,這里提到的兩個操作既可以是在一個線程之內(nèi)形纺,也可以是在不同線程之間。
常見的 happens-before 規(guī)則如下:
- 程序順序規(guī)則:一個線程中的每個操作 happens-before 于該線程中的任意后續(xù)操作徒欣。
- 監(jiān)視器鎖規(guī)則:對一個鎖的解鎖 happens-before 于隨后對這個鎖的加鎖逐样。
-
volatile
規(guī)則:對于一個volatile
域的寫 happens-before 于任意后續(xù)對這個volatile
域的讀。對于volatile
的詳細理解可以看我的另一篇文章打肝。 - 傳遞性:如果 A happens-before B脂新,且 B happens-before C,那么 A happens-before C粗梭。
一個 happens-before 規(guī)則對應(yīng)于一個或多個編譯器和處理器重排序規(guī)則争便。對于 Java 程序員來說,happens-before 規(guī)則簡單易懂楼吃,它避免了 Java 程序員為了理解 JMM 提供的內(nèi)存可見性保證而去學習復雜的重排序規(guī)則以及這些規(guī)則的具體實現(xiàn)方法始花。
重排序
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段妄讯。
數(shù)據(jù)依賴性
如果兩個操作訪問同一個變量,并且這兩個操作中有一個為寫操作酷宵,此時著兩個操作之間就存在數(shù)據(jù)依賴性亥贸。數(shù)據(jù)依賴性分為下面 3 種類型:
名稱 | 代碼示例 | 說明 |
---|---|---|
寫后讀 | a = 1;b = a浇垦; | 寫一個變量之后炕置,再讀這個變量 |
寫后寫 | a = 1; a = 2男韧; | 寫一個變量之后再寫這個變量 |
讀后寫 | a = b朴摊; b = 1; | 讀一個變量之后再寫這個變量 |
上面 3 種情況此虑,只要重排序兩個操作的執(zhí)行順序甚纲,程序的執(zhí)行結(jié)果就會被改變。
前面提到過朦前,編譯器和處理器可能會對操作做重排序介杆。編譯器和處理器在重排序時會遵守數(shù)據(jù)依賴性,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序韭寸。這里所說的數(shù)據(jù)依賴性僅針對單個處理器中執(zhí)行的指令序列和單個線程中執(zhí)行的操作春哨,不同的處理器和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
as-if-serial 語義
as-if-serial 語義的意思是:不管怎么重排序恩伺,(單線程)程序的執(zhí)行結(jié)果不能被改變赴背。編譯器、 runtime 和 處理器都必須遵守晶渠。
為了遵守 as-if-serial 語義凰荚,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因為這種重排序會改變程序的執(zhí)行結(jié)果乱陡,但是如果操作之間不存在數(shù)據(jù)依賴關(guān)系浇揩,這些操作就可能被編譯器和處理器重排序。
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r;// C
上面的代碼片段中憨颠, A 和 C 之間存在數(shù)據(jù)依賴關(guān)系, B 和 C 之間也存在數(shù)據(jù)依賴關(guān)系积锅,因此在最終執(zhí)行的序列中爽彤, C 不能被重排序到 A 和 B 的前面,但 A 和 B 之間不存在數(shù)據(jù)依賴關(guān)系缚陷,編譯器和處理器可以重排序 A 和 B 之間的執(zhí)行順序适篙。
as-if-serial 語義把單線程程序保護了起來,遵守 as-if-serial 語義的編譯器箫爷、runtime 和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個幻覺:單線程程序是按程序的順序來執(zhí)行的嚷节。as-if-serial 語義使單線程程序員無需擔心重排序會干擾他們聂儒,也無需擔心內(nèi)存可見性問題。
程序順序規(guī)則
根據(jù) happens-before 規(guī)則硫痰,上面計算圓的面積的示例代碼存在 3 個 happens-before 關(guān)系衩婚。
- A happens-before B。
- B happens-before C效斑。
- A happens-before C非春。
這里 A happens-before B,但實際執(zhí)行時 B 卻可以排在 A 之前執(zhí)行缓屠。如果 A happens-before B奇昙,JMM 并不要求 A 一定要在 B 之前執(zhí)行, JMM 只要求前一個操作對后一個操作可見敌完,這里操作 A 的執(zhí)行結(jié)果不需要對操作 B 可見储耐,而且重排序操作 A 和操作 B的執(zhí)行結(jié)果與操作 A 和操作 B 按 happens-before 順序執(zhí)行的結(jié)果一致。在這種情況下滨溉, JMM 會認為這種重排序并不非法弧岳, JMM 允許這種重排序。
在計算機中业踏,軟件技術(shù)和硬件技術(shù)有一個共同目標:在不改變程序執(zhí)行結(jié)果的前提下禽炬,盡可能并行度。編譯器勤家、處理器和 JMM 都遵循這一規(guī)則腹尖。
順序一致性
順序一致性內(nèi)存模型是一個理論參考模型,在設(shè)計的時候伐脖,處理器的內(nèi)存模型和編程語言的內(nèi)存模型都會以順序一致性內(nèi)存模型作為參照热幔。
數(shù)據(jù)競爭與順序一致性
當程序未正確同步時,就可能會存在數(shù)據(jù)競爭讼庇。 JMM 對正確同步的多線程程序的內(nèi)存一致性做了如下保證绎巨。
如果程序是正確同步的,線程的執(zhí)行將具有順序一致性——即程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同蠕啄。
順序一致性內(nèi)存模型
順序一致性內(nèi)存模型是一個被計算機科學家理想化了的理論參考模型场勤,它為程序員提供了極強的內(nèi)存可見性保證。順序一致性內(nèi)存模型有倆大特性歼跟。
- 一個線程中的所有操作必須按照程序的順序來執(zhí)行和媳。
- 不管程序是否同步,所有線程都只能看到一個單一的操作執(zhí)行順序哈街,在順序一致性內(nèi)存模型中留瞳,每個操作都必須原子執(zhí)行且立刻對所有線程可見。
在概念上骚秦,順序一致性內(nèi)存模型有一個單一的全局內(nèi)存她倘,這個內(nèi)存通過一個左右擺動的開關(guān)可以連接到任意一個線程璧微,同時每一個線程必須按照程序的順序來執(zhí)行內(nèi)存讀/寫操作。
總結(jié)
前面對 Java 內(nèi)存模型的基礎(chǔ)知識和內(nèi)存模型的具體實現(xiàn)進行了說明硬梁。下面對 Java 內(nèi)存模型相關(guān)知識做一個總結(jié)前硫。
JMM 是一個語言級的內(nèi)存模型,處理器內(nèi)存模型是硬件級的內(nèi)存模型靶溜,順序一致性內(nèi)存模型是一個理論參考模型开瞭。 JMM 和處理器內(nèi)存模型在設(shè)計時通常會以順序一致性內(nèi)存模型作為參照,在設(shè)計時 JMM 和處理器內(nèi)存模型會對順序一致性模型做一些放松罩息,因為如果完全按照順序一致性模型來實現(xiàn) JMM 和處理器內(nèi)存模型嗤详,那么很多的編譯器和處理器優(yōu)化都要被禁止,這對程序的執(zhí)行性能將有很大的影響瓷炮。