筆者知識和理解有限,歡迎指出不足之處java
在併發編程中,咱們須要處理兩個關鍵問題程序員
這裏的線程是指併發執行的活動實體。通訊是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通訊機制有兩種:<span style="color:blue">共享內存、消息傳遞</span>。編程
在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊。數組
在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊。緩存
同步是指程序用於控制不一樣線程之間操做發生相對順序的機制。多線程
在共享內存併發模型裏,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。併發
在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。app
<span style="color:blue">Java 的併發採用的是共享內存模型</span>,Java線程之間的通訊老是隱式進行(注意不是說同步是隱式,而是說共享內存在Java中是隱式進行),整個通訊過程對程序員徹底透明。若是編寫多線程程序的Java程序員不理解隱式進行的線程之間通訊的工做機制,極可能會遇到各類奇怪的內存可見性問題。性能
用一張圖總結併發編程模型:
學習
<span style="color:blue">在java中,全部實例域、靜態域和數組元素存儲在堆內存中,堆內存在線程之間共 享(本文使用「共享變量」這個術語代指實例域,靜態域和數組元素)。</span>
局部變量 (Local variables),方法定義參數(java語言規範稱之爲formal method parameters)和異常處理器參數(exception handler parameters)不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。
Java 線程之間的通訊由 Java 內存模型(本文簡稱爲 JMM)控制,JMM 決定一個 線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,<span style="color:blue">JMM定義了線程和主內存之間的抽象關係:
<span style="color:red">線程之間的共享變量存儲在主內存(main
memory)中</span>,<span style="color:red">每一個線程都有一個私有的本地內存(local memory)</span>,本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是 JMM 的一個抽象概念,並不 真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。</span>
Java 內存模型的抽象示意圖以下:
名詞解釋:
從上圖來看,線程 A 與線程 B 之間如要通訊的話,必需要經歷下面 2 個步驟:
下面經過示意圖來講明這兩個步驟:
如上圖所示,本地內存 A 和 B 有主內存中共享變量 m 的副本。
假設初始時,這三個 內存中的 m 值都爲 0。線程 A 在執行時,把更新後的 m 值(假設值爲 1)臨時存放 在本身的本地內存 A 中。當線程 A 和線程 B 須要通訊時,線程 A 首先會把本身本地內存中修改後的 m 值刷新到主內存中,此時主內存中的 m 值變爲了 1。隨後,線程 B 到主內存中去讀取線程 A 更新後的 m 值,此時線程 B 的本地內存的 m 值也變 爲了 1。
從總體來看,這兩個步驟實質上是線程 A 在向線程 B 發送消息,並且這個通訊過程 必需要通過主內存。JMM 經過控制主內存與每一個線程的本地內存之間的交互,<span style="color:blue">來爲 java 程序員提供內存可見性保證。</span>
### 重排序
在執行程序時爲了提升性能,編譯器和處理器經常會對指令作重排序。重排序分三
種類型:
編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。
現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。
從 java 源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
上述的<span style="color:blue">編譯器優化重排序</span>屬於編譯器重排序,<span style="color:blue">指令級並行重排序</span>和<span style="color:blue">內存系統重排序</span>屬於處理器重排序。
這些重排序均可能會致使多線程程序出現內存可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是全部的編譯器重排序都要禁止)。
對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類 型的內存屏障(memory barriers,intel 稱之爲 memory fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序(不是全部的處理器重排序都要禁 止)。
JMM 屬於語言級的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。
現代的處理器使用寫緩衝區來臨時保存向內存寫入的數據。寫緩衝區能夠保證指令流水線持續運行,它能夠避免因爲處理器停頓下來等待向內存寫入數據而產生的延 遲。
同時,經過以批處理的方式刷新寫緩衝區,以及合併寫緩衝區中對同一內存地址的屢次寫,能夠減小對內存總線的佔用。
雖然寫緩衝區有這麼多好處,但每一個處理器上的寫緩衝區,僅僅對它所在的處理器可見。這個特性會對內存操做的執行順序產生重要的影響:處理器對內存的讀/寫操做的執行順序,不必定與內存實際發生的讀/寫操做順序一致!爲了具體說明,請看下面示例:
假設處理器 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 同樣,這裏就不贅述了)。
這裏的關鍵是,因爲寫緩衝區僅對本身的處理器可見,它會致使處理器執行內存操 做的順序可能會與內存實際的操做執行順序不一致。因爲現代的處理器都會使用寫緩衝區,所以現代的處理器都會容許對寫-讀操做重排序。
下面是常見處理器容許的重排序類型的列表:
上表單元格中的「N」表示處理器不容許兩個操做重排序,「Y」表示容許重排序。
從上表咱們能夠看出:
常見的處理器都容許 Store-Load 重排序。
常見的處理器都不容許對存在數據依賴的操做作重排序。
sparc-TSO 和 x86 擁有相對較強的處理器內存模型,它們僅容許對寫-讀操做作重排序(由於它們都使用了寫緩衝區)。
爲了保證內存可見性,java 編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM 把<span style="color:blue">內存屏障</span>指令分爲下列四類:
屏障類型 | 指令示例 | 說明 | |
---|---|---|---|
LoadLoad | Load1;LoadLoad;Load2 | 確保Load1數據裝載在Load2以及Load2以後的裝載指令的裝載。 | |
StoreStore | Store1;StoreStore;Store2 | 確保 Store1 數據對其餘處理器可見(刷新到內存),以前於 Store2 及全部後續存儲指令的存儲。 | |
LoadStore | Load1;LoadStore;Store2 | 確保 Load1 數據裝載,以前於 Store2 及全部後續的存儲指令刷 新到內存。 | |
StoreLoad | Store1;StoreLoad;Load2 | 確保 Store1 數據對其餘處理器變 得可見(指刷新到內存),以前於 Load2 及全部後續裝載指令的裝 載。StoreLoad Barriers 會使該屏 障以前的全部內存訪問指令(存儲 和裝載指令)完成以後,才執行該 屏障以後的內存訪問指令。 |
StoreLoad屏障類型是一個「全能型」的屏障,它同時具備其餘三個屏障的效果。 現代的多處理器大都支持該屏障(其餘類型的屏障不必定被全部處理器支持)。執 行該屏障開銷會很昂貴,由於當前處理器一般要把寫緩衝區中的數據所有刷新到內 存中(buffer fully flush)。
從 JDK5 開始,java使用新的JSR-133內存模型(本文除非特別說明,針對的都 是 JSR- 133 內存模型)。JSR-133 使用 happens-before 的概念來闡述操做之間 的內存可見性。
在 JMM 中,若是一個操做執行的結果須要對另外一個操做可見,那 麼這兩個操做之間必需要存在 happens-before 關係。這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間。
與程序員密切相關的 happens-before 規則以下:
一個線程中的每一個操做,happens-before於該線程中的任意後續操做。
對一個監視器的解鎖,happens-before於隨後對這個監視器 的加鎖。
對一個 volatile 域的寫,happens-before於任意後續對 這個 volatile 域的讀。
若是 A happens- before B,且 B happens- before C,那麼 A happens- before C。
注意,兩個操做之間具備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 規則簡單易懂,它避免 java 程序 員爲了理解JMM提供的內存可見性保證而去學習複雜的重排序規則以及這些規則的具體實現。
這篇文章主要是給你們澄清一些概念,爲後續的文章展開分析作好基礎工做。主要講了
這篇文章,你們着重理解這些基礎概念,並把原理圖理清便可。