平時我們很少會注意Java內存模型,對于一些概念很多都是背誦,不是甚解,納悶這一章,將把這個透明層給扯開,讓他再也遮不住我們眼睛。
首先兩個關鍵問題
1)線程之間如何通信
2)線程之間是如何同步
線程之間的通信機制有兩種:共享內存和消息傳遞。
在共享內存的并發模型中,線程之間共享程序的公共狀態,通過讀-寫內存中的公共狀態進行隱式通信。
在消息傳遞的并發模型中,線程之間沒有公共狀態,線程之間必須通過發送消息來進行顯示通信。
同步是指程序中用于控制不同線程間發生相對順序的機制,在共享內存并發模型里,同步是顯示進行的。程序員必須顯示指定某個方法或某段代碼需要在線程之間互相執行。在消息傳遞的并發模型里,由于消息的發送必須在消息的接受之前,因此同步是隱式進行的。
那么Java的并發采用的共享內存模型,Java線程之間的通信是隱式進行的。整個通信的過程對程序員是完全透明的。如果我們不理解這個隱式進行的線程之間的這種通信,那么遇到各種奇怪的內存問題也就不奇怪了。
Java內存模型的抽象結構
首先,我們要了解一下內存可見性問題發生的位置。在Java中,所有實例域和數組元素都存儲在堆內存中(重點),堆內存在線程之間共享。而局部變量,方法定義參數和異常處理的參數不會在線程之間共享的,那是線程私有的,所以他們不會有內存可見性問題,也不受內存模型的影響。
Java線程之間的通信由Java內存模型(JMM)控制。JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來說,JMM定義了線程和主內存之間的抽象關系。線程之間的共享變量存儲在主內存中(Main Memory)每個線程都有一個私有的本地存儲(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存、寫緩沖區、寄存器以及其他的硬件和編譯器優化。就是我們在理解中可以想象有一個本地存儲這個東西,方便我們去理解和想象,但是實際是緩存、寫緩沖區、寄存器等形式。
如下圖一般,我們就能很好地理解這個概念了。
從圖中我們可以發現:如果線程A要與線程B通信的話,需要下面兩個過程:
1)線程A把本地內存中A中更新的共享變量刷新到主內存中
2)線程B到主內存中去讀取線程A已經更新過的共享變量
我們會發現,共享內存之間的通信確實是隱性的。
當然我們在舉個小栗子,更直觀的去理解。
相信大家都看懂了吧。線程A向線程B發送消息,必須通過主內存,JMM通過控制主內存和每個線程的本地內存之間的交互,來為Java程序員提供內存可見性保證。
從源代碼到執行序列的重排序
相信大家都聽過重排序這個名詞,就是在執行程序的時候,為了提高性能,編譯器和處理器常常會對指令做重排序。
重排序分為三種:
1)編譯器優化的重排序。編譯器在不改變單線程程序語句的前提下,可以重新安排語句的執行順序。
2)指令級并行的重排序?,F代處理器采用了指令級并行技術來將多條指令重疊執行。如果不存在數據的依賴性,處理器可能改變語句對應的機器指令的執行順序。
3)內存系統的重排序。由于處理器使用了緩存和讀/寫緩沖區,這使得加載和存儲操作看上去是亂寫執行。
從Java源代碼到最終執行的指令序列,會分別經歷下面3種重排序。
其中1屬于編譯器重排序,2和3屬于處理器重排序。這些排序都有可能造成多線程程序出現內存可見性問題。
對于編譯器JMM的編譯器重排序規則可以禁止特定類型的編譯器重排序。
對于處理器則需要通過內存屏障指令來禁止特定類型的處理器重排序。
JMM屬于語言級的內存模型,他確保在不同的編譯器和不同的處理器上,通過禁止特定類型的編譯器重排序和處理器重排序,為我們提供一致的內存可見性保證。
接下來講點有趣的而且很有用的東西。比上面的好玩很多。
并發編程模型的分類
背景:
現代的處理器使用寫緩沖區臨時保存向內存寫入的數據。寫緩沖區可以保證指令流水線持續運行,避免由于處理器停頓下來等待內存寫入數據而產生的延遲(cpu比內存塊太多了,所以CPU會經常等待內存寫入數據)。同時,通過以批處理的方式刷新寫緩沖區,以及合并寫緩沖區對同一內存地址的多次寫,減少對內存總線的占用。我們發現寫緩沖區的優點很給力啊,但是···它有一個很嚴重的問題,因為每個處理器都有自己的寫緩沖區,而且只對它所在的處理器可見。這一特性會對內存操作的執行順序帶來重要的影響。處理器對內存的讀/寫操作的執行順序,不一定與內存實際發生的讀/寫操作順序一致。
不太懂不要緊,來一口小栗子馬上懂
線程A、B分別進行A1、A2和B1、B2的操作,如果是一個線程執行這幾個操作,是不會有問題的,但是兩個線程,分別進行操作,因為重排序造成執行順序的不確定性,我們很有可能得到x=y=0 的結果。
其中步驟如下圖,首先同時A1、B1 寫緩沖區操作之后,直接讀取共享變量(A2、B2)。最后才會把臟數據刷新。這種順序就會得到x=y=0 的結果。
這種順序發生的關鍵在于寫緩沖區僅對自己的處理器可見,他會導致處理器執行內存操作的順序可能會與內存實際的操作順序不一樣。由于現代的處理器都會使用寫緩沖區,因此現代的處理器都會允許對寫-讀操作進行重排序。
重點就是現代處理器允許讀-寫操作進行重排序。
總結上圖的結論 :常見的處理下允許Store-Load重排序,這個不影響結果。但是不允許數據依賴的操作進行重排序。
為了保證內存的可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。大家了解一下就好了。不必太多糾結。
最后,介紹一個和我們息息相關的透明知識點。
happens-before
從JDK5開始,Java采用JSR-133內存模型,JSR-133使用happens-before的概念來闡述操作之間的內存可見性。在JMM中,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須存在happens-before關系。這里提到的兩個操作既可以在一個線程里也可以是在不同的線程之間。
happens-before規則:
1)程序順序規則:一個線程中的每個操作,happens-before于該線程中的任意后續操作。
意思就是當在單線程中,程序中的邏輯上前一個操作必須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 is visible to and ordered before the second)
還是很繞對吧,就是這樣的如果
public void add(){
int a = 1;//A
int b= 2;//B
int c = a+b;//C
}
A happens-before B ,A happens-before C,B happens-before C。
因為A happens-before B,所以A操作產生的結果一定要對B操作可見,但是B操作和A沒關系,所以可以重排序。同樣A happens-before C,所以A操作產生的結果一定要對C操作可見,如果重排序了結果會錯誤,所以不能重排序。
再來體會一遍:如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須存在happens-before關系。兩個操作之間具有happens-before關系,并不意味前一個操作必須要在后一個操作之前執行!happens-before只是要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前。