在併發編程中,咱們須要處理兩個關鍵問題:線程之間如何通訊及線程之間如何同步(這裏的線程是指併發執行的活動實體)。通訊是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通訊機制有兩種:共享內存和消息傳遞。html
在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊。在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊。java
同步是指程序用於控制不一樣線程之間操做發生相對順序的機制。在共享內存併發模型裏,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。linux
Java的併發採用的是共享內存模型,Java線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明。若是編寫多線程程序的Java程序員不理解隱式進行的線程之間通訊的工做機制,極可能會遇到各類奇怪的內存可見性問題。程序員
在java中,全部實例域、靜態域和數組元素存儲在堆內存中,堆內存在線程之間共享(本文使用「共享變量」這個術語代指實例域,靜態域和數組元 素)。局部變量(Local variables),方法定義參數(java語言規範稱之爲formal method parameters)和異常處理器參數(exception handler parameters)不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。編程
Java線程之間的通訊由Java內存模型(本文簡稱爲JMM)控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來 看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其 他的硬件和編譯器優化。Java內存模型的抽象示意圖以下:數組
從上圖來看,線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:緩存
下面經過示意圖來講明這兩個步驟:多線程
如上圖所示,本地內存A和B有主內存中共享變量x的副本。假設初始時,這三個內存中的x值都爲0。線程A在執行時,把更新後的x值(假設值爲1) 臨時存放在本身的本地內存A中。當線程A和線程B須要通訊時,線程A首先會把本身本地內存中修改後的x值刷新到主內存中,此時主內存中的x值變爲了1。隨 後,線程B到主內存中去讀取線程A更新後的x值,此時線程B的本地內存的x值也變爲了1。併發
從總體來看,這兩個步驟實質上是線程A在向線程B發送消息,並且這個通訊過程必需要通過主內存。JMM經過控制主內存與每一個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。app
在執行程序時爲了提升性能,編譯器和處理器經常會對指令作重排序。重排序分三種類型:
從java源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序均可能會致使多線程程序出現內存可見性問題。對於編譯器,JMM的編譯器重排序規 則會禁止特定類型的編譯器重排序(不是全部的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列 時,插入特定類型的內存屏障(memory barriers,intel稱之爲memory fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序(不是全部的處理器重排序都要禁止)。
JMM屬於語言級的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。
現代的處理器使用寫緩衝區來臨時保存向內存寫入的數據。寫緩衝區能夠保證指令流水線持續運行,它能夠避免因爲處理器停頓下來等待向內存寫入數據而 產生的延遲。同時,經過以批處理的方式刷新寫緩衝區,以及合併寫緩衝區中對同一內存地址的屢次寫,能夠減小對內存總線的佔用。雖然寫緩衝區有這麼多好處, 但每一個處理器上的寫緩衝區,僅僅對它所在的處理器可見。這個特性會對內存操做的執行順序產生重要的影響:處理器對內存的讀/寫操做的執行順序,不必定與內 存實際發生的讀/寫操做順序一致!爲了具體說明,請看下面示例:
Processor A | Processor B |
---|---|
a = 1; //A1x = b; //A2 | b = 2; //B1y = a; //B2 |
lspan="2">初始狀態:a = b = 0處理器容許執行後獲得結果:x = y = 0 |
假設處理器A和處理器B按程序的順序並行執行內存訪問,最終卻可能獲得x = y = 0的結果。具體的緣由以下圖所示:
這裏處理器A和處理器B能夠同時把共享變量寫入本身的寫緩衝區(A1,B1),而後從內存中讀取另外一個共享變量(A2,B2),最後才把本身寫緩存區中保存的髒數據刷新到內存中(A3,B3)。當以這種時序執行時,程序就能夠獲得x = y = 0的結果。
從內存操做實際發生的順序來看,直處處理器A執行A3來刷新本身的寫緩存區,寫操做A1纔算真正執行了。雖然處理器A執行內存操做的順序 爲:A1->A2,但內存操做實際發生的順序倒是:A2->A1。此時,處理器A的內存操做順序被重排序了(處理器B的狀況和處理器A同樣, 這裏就不贅述了)。
這裏的關鍵是,因爲寫緩衝區僅對本身的處理器可見,它會致使處理器執行內存操做的順序可能會與內存實際的操做執行順序不一致。因爲現代的處理器都會使用寫緩衝區,所以現代的處理器都會容許對寫-讀操作重排序。
下面是常見處理器容許的重排序類型的列表:
Load-Load | Load-Store | Store-Store | Store-Load | 數據依賴 | |
sparc-TSO | N | N | N | Y | N |
x86 | N | N | N | Y | N |
ia64 | Y | Y | Y | Y | N |
PowerPC | Y | Y | Y | Y | N |
上表單元格中的「N」表示處理器不容許兩個操做重排序,「Y」表示容許重排序。
從上表咱們能夠看出:常見的處理器都容許Store-Load重排序;常見的處理器都不容許對存在數據依賴的操做作重排序。sparc-TSO和x86擁有相對較強的處理器內存模型,它們僅容許對寫-讀操做作重排序(由於它們都使用了寫緩衝區)。
※注1:sparc-TSO是指以TSO(Total Store Order)內存模型運行時,sparc處理器的特性。
※注2:上表中的x86包括x64及AMD64。
※注3:因爲ARM處理器的內存模型與PowerPC處理器的內存模型很是相似,本文將忽略它。
※注4:數據依賴性後文會專門說明。
爲了保證內存可見性,java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM把內存屏障指令分爲下列四類:
屏障類型 | 指令示例 | 說明 |
LoadLoad Barriers | Load1; LoadLoad; Load2 | 確保Load1數據的裝載,以前於Load2及全部後續裝載指令的裝載。 |
StoreStore Barriers | Store1; StoreStore; Store2 | 確保Store1數據對其餘處理器可見(刷新到內存),以前於Store2及全部後續存儲指令的存儲。 |
LoadStore Barriers | Load1; LoadStore; Store2 | 確保Load1數據裝載,以前於Store2及全部後續的存儲指令刷新到內存。 |
StoreLoad Barriers | Store1; StoreLoad; Load2 | 確保Store1數據對其餘處理器變得可見(指刷新到內存),以前於Load2及全部後續裝載指令的裝載。StoreLoad Barriers會使該屏障以前的全部內存訪問指令(存儲和裝載指令)完成以後,才執行該屏障以後的內存訪問指令。 |
StoreLoad Barriers是一個「全能型」的屏障,它同時具備其餘三個屏障的效果。現代的多處理器大都支持該屏障(其餘類型的屏障不必定被全部處理器支持)。執行 該屏障開銷會很昂貴,由於當前處理器一般要把寫緩衝區中的數據所有刷新到內存中(buffer fully flush)。
從JDK5開始,java使用新的JSR -133內存模型(本文除非特別說明,針對的都是JSR- 133內存模型)。JSR-133提出了happens-before的概念,經過這個概念來闡述操做之間的內存可見性。若是一個操做執行的結果須要對另 一個操做可見,那麼這兩個操做之間必須存在happens-before關係。這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間。 與程序員密切相關的happens-before規則以下:
注意,兩個操做之間具備happens-before關係,並不意味着前一個操做必需要在後一個操做以前執行!happens-before僅僅 要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前(the first is visible to and ordered before the second)。happens- before的定義很微妙,後文會具體說明happens-before爲何要這麼定義。
happens-before與JMM的關係以下圖所示:
如上圖所示,一個happens-before規則一般對應於多個編譯器重排序規則和處理器重排序規則。對於java程序員來 說,happens-before規則簡單易懂,它避免程序員爲了理解JMM提供的內存可見性保證而去學習複雜的重排序規則以及這些規則的具體實現。