3.1.1 併發編程模型的兩個關鍵問題程序員
在併發編程中,須要處理兩個關鍵問題:線程之間如何通訊及線程之間如何同步(這裏的編程
線程是指併發執行的活動實體)。通訊是指線程之間以何種機制來交換信息。在命令式編程segmentfault
中,線程之間的通訊機制有兩種:共享內存和消息傳遞。緩存
共享內存模式:經過寫-讀內存中的公共狀態進行隱式通訊多線程
消息傳遞模式:若是線程之間沒有公共狀態,線程之間必須經過發送消息來顯式進行通訊。併發
同步是指程序中用於控制不一樣線程間操做發生相對順序的機制。在共享內存併發模型app
裏,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。性能
在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。學習
Java的併發採用的是共享內存模型,Java線程之間的通訊老是隱式進行,整個通訊過程對優化
程序員徹底透明。若是編寫多線程程序的Java程序員不理解隱式進行的線程之間通訊的工做
機制,極可能會遇到各類奇怪的內存可見性問題。
3.1.2 Java內存模型的抽象結構
Java線程之間的通訊由Java內存模型(本文簡稱爲JMM)控制,JMM決定一個線程對共享
變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽
象關係:線程之間的共享變量存儲在主內存(Main Memory)中,每一個線程都有一個私有的本地
內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的
一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器以及其餘的硬件和編譯器優
化。Java內存模型的抽象示意如圖3-1所示。
3.1.3 從源代碼到指令序列的重排序
在執行程序時,爲了提升性能,編譯器和處理器經常會對指令作重排序。重排序分3種類
型。
1)編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句
的執行順序。
2)指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level
Parallelism,ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應
機器指令的執行順序。
3)內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上
去多是在亂序執行。
從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序,如圖3-3所示。
上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序可能會致使多線程程序
出現內存可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排
序(不是全部的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要
求Java編譯器在生成指令序列時,插入特定類型的內存屏障(Memory Barriers,Intel稱之爲
Memory Fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序。
JMM屬於語言級的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁
止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。
3.1.4 併發編程模型的分類
現代的處理器使用寫緩衝區臨時保存向內存寫入的數據。寫緩衝區能夠保證指令流水線
持續運行,它能夠避免因爲處理器停頓下來等待向內存寫入數據而產生的延遲。同時,經過以
批處理的方式刷新寫緩衝區,以及合併寫緩衝區中對同一內存地址的屢次寫,減小對內存總
線的佔用。雖然寫緩衝區有這麼多好處,但每一個處理器上的寫緩衝區,僅僅對它所在的處理器
可見。這個特性會對內存操做的執行順序產生重要的影響:處理器對內存的讀/寫操做的執行
順序,不必定與內存實際發生的讀/寫操做順序一致。這裏的關鍵是,因爲寫緩衝區僅對本身的處理器可見,它會致使處理器執行內存操做的順序可能會與內存實際的操做執行順序不一致。因爲現代的處理器都會使用寫緩衝區,所以現代的處理器都會容許對寫-讀操做進行重排序。
爲了保證內存可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁
止特定類型的處理器重排序。JMM把內存屏障指令分爲4類,如表3-3所示。
StoreLoad Barriers是一個「全能型」的屏障,它同時具備其餘3個屏障的效果。現代的多處
理器大多支持該屏障(其餘類型的屏障不必定被全部處理器支持)。執行該屏障開銷會很昂
貴,由於當前處理器一般要把寫緩衝區中的數據所有刷新到內存中(Buffer Fully Flush)。
3.1.5 happens-before簡介
從JDK 5開始,Java使用新的JSR-133內存模型(除非特別說明,本文針對的都是JSR-133內
存模型)。JSR-133使用happens-before的概念來闡述操做之間的內存可見性。在JMM中,若是一
個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必需要存在happens-before關
系。這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間。
與程序員密切相關的happens-before規則以下。
·程序順序規則:一個線程中的每一個操做,happens-before於該線程中的任意後續操做。
·監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
·volatile變量規則:對一個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的關係如圖3-5所示。
如圖3-5所示,一個happens-before規則對應於一個或多個編譯器和處理器重排序規則。對
於Java程序員來講,happens-before規則簡單易懂,它避免Java程序員爲了理解JMM提供的內存
可見性保證而去學習複雜的重排序規則以及這些規則的具體實現方法。
ps.關於Happens-Before具體細則能夠參考這篇博客:從Java多線程可見性談Happens-Before原則。