在併發編程中,咱們須要處理兩個關鍵問題:線程之間如何通訊及線程之間如何同步(這裏的線程是指併發執行的活動實體).通訊是指線程之間以何種機制來交換信息.在命令式編程中,線程之間的通訊機制有兩種:共享內存和消息傳遞.java
在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊.在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯示進行通訊.程序員
同步是指程序用於控制不一樣線程之間操做發生相對順序的機制.在共享內存併發模型裏,同步是顯示進行的.程序員必須顯示指定某個方法或某段代碼須要在線程之間互斥執行.在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式執行的.編程
java的併發採用的是共享內存模型,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個步驟:app
1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去.性能
2. 而後,線程B到主內存中去讀取線程A之間已更新過的共享變量.學習
下面經過示意圖來講明這兩個步驟:
如上圖所示,本地內存A和B有主內存中共享變量X的副本.假設初始時,這三個內存中的X值都爲0.線程A在執行時,把更新後的值(假設值爲1)臨時存放在本身的本地內存A中.當線程A和線程B須要通訊時,線程A首先把本身本地內存中修改後的x值刷新到主內存中,此時主內存中的x值變爲了1.隨後線程B到主內存中去讀取線程A更新後的值.此時線程B的本地內存x值也變爲了1.
從總體來看,這兩個步驟實質上是線程A向線程B發送消息,並且這個通訊過程必需要通過主內存.JMM經過控制主內存與每一個線程的本地內存之間的交互,來java程序員提供內存可見性的保證.
重排序
在執行程序時爲了提升性能,編譯器和處理器經常會對指令作重排序.重排序分三種類型:
1. 編譯器優化的重排序.編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序.
2. 指令級並行的重排序.如今處理器採用了指令級並行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行.若是不存在數據依賴性,處理器能夠改變語句對機器指令的執行順序.
3. 內存系統的重排序.因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行.
從java源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
上述的1屬於編譯器重排序,2和3屬於處理器重排序.這些重排序均可能會致使多線程程序出現內存可見性問題.對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是全部的編譯器重排序都要禁止).對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel稱之爲memory fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序(不是全部的處理器重排序都要禁止).
JMM屬於語言級的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證.
happens-before
從JDK5開始,Java使用新的JSR-133內存模型(本文除特別說明,針對的都是JSR-133內存模型).JSR-133提出了happens-before的概念,經過這個概念來闡述操做之間的內存可見性.若是一個操做執行的結果須要對另一個操做可見,那麼這兩個操做之間必須存在happens-before關係.這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間.與程序員密切相關的happens-before規則以下:
1. 程序順序規則:一個線程中的每一個操做,happens-before於該線程中的任意後續操做
2. 監視器鎖規則:對一個監視器鎖的解鎖,happens-before於隨後對這個監視器鎖的枷鎖
3. volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀
4. 傳遞性:若是A happens-before B,且B happens-before C,那麼A happens-before C
注意,兩個操做之間具備happens-before關係,並不意味着前一個操做必需要在後一個操做以前執行!happens-before僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前(the first visible to and ordered before second).
happens-before與JMM的關係以下圖所示:
如上圖所示,一個happens-before規則一般對應於多個編譯器重排序和處理器重排序規則.對於java程序員來講,happens-before規則簡單易懂,它避免程序員爲了理解JMM提供的內存可見性保證而去學習複雜的重排序規則以及這些規則的具體實現.