📦 本文以及示例源碼已歸檔在 javacorehtml
Java 內存模型(Java Memory Model),簡稱 JMM。java
JVM 中試圖定義一種 JMM 來屏蔽各類硬件和操做系統的內存訪問差別,以實現讓 Java 程序在各類平臺下都能達到一致的內存訪問效果。git
物理機遇到的併發問題與虛擬機中的狀況有很多類似之處,物理機對併發的處理方案對於虛擬機的實現也有至關大的參考意義。github
物理內存的第一個問題是:硬件處理效率。編程
高速緩存解決了 硬件效率問題,可是引入了一個新的問題:緩存一致性(Cache Coherence)。緩存
在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存。當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致。安全
爲了解決緩存一致性問題,須要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操做。markdown
除了高速緩存之外,爲了使得處理器內部的運算單元儘可能被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化。處理器會在計算以後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但不保證程序中各個語句計算的前後順序與輸入代碼中的順序一致。多線程
亂序執行技術是處理器爲提升運算速度而作出違背代碼原有順序的優化。架構
內存模型
這個概念。咱們能夠理解爲:在特定的操做協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象。不一樣架構的物理計算機能夠有不同的內存模型,JVM 也有本身的內存模型。
JVM 中試圖定義一種 Java 內存模型(Java Memory Model, JMM)來屏蔽各類硬件和操做系統的內存訪問差別,以實現讓 Java 程序 在各類平臺下都能達到一致的內存訪問效果。
JMM 的主要目標是 定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量(Variables)與 Java 編程中所說的變量有所區別,它包括了實例字段、靜態字段和構成數值對象的元素,但不包括局部變量與方法參數,由於後者是線程私有的,不會被共享,天然就不會存在競爭問題。爲了得到較好的執行效能,JMM 並無限制執行引擎使用處理器的特定寄存器或緩存來和主存進行交互,也沒有限制即便編譯器進行調整代碼執行順序這類優化措施。
JMM 規定了全部的變量都存儲在主內存(Main Memory)中。
每條線程還有本身的工做內存(Working Memory),工做內存中保留了該線程使用到的變量的主內存的副本。工做內存是 JMM 的一個抽象概念,並不真實存在,它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。
線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣的線程間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。
說明:
這裏說的主內存、工做內存與 Java 內存區域中的堆、棧、方法區等不是同一個層次的內存劃分。
相似於物理內存模型面臨的問題,JMM 存在如下兩個問題:
as-if-serial
屬性。通俗地說,就是在單線程狀況下,要給程序一個順序執行的假象。即通過重排序的執行結果要與順序執行的結果保持一致。 JMM 定義了 8 個操做來完成主內存和工做內存之間的交互操做。JVM 實現時必須保證下面介紹的每種操做都是 原子的(對於 double 和 long 型的變量來講,load、store、read、和 write 操做在某些平臺上容許有例外 )。
lock
(鎖定) - 做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。 unlock
(解鎖) - 做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。 read
(讀取) - 做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的 load
動做使用。 write
(寫入) - 做用於主內存的變量,它把 store 操做從工做內存中獲得的變量的值放入主內存的變量中。 load
(載入) - 做用於工做內存的變量,它把 read 操做從主內存中獲得的變量值放入工做內存的變量副本中。 use
(使用) - 做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值得字節碼指令時就會執行這個操做。 assign
(賦值) - 做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。 store
(存儲) - 做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後 write
操做使用。 若是要把一個變量從主內存中複製到工做內存,就須要按序執行 read
和 load
操做;若是把變量從工做內存中同步回主內存中,就須要按序執行 store
和 write
操做。但 Java 內存模型只要求上述操做必須按順序執行,而沒有保證必須是連續執行。
JMM 還規定了上述 8 種基本操做,須要知足如下規則:
上文介紹了 Java 內存交互的 8 種基本操做,它們遵循 Java 內存三大特性:原子性、可見性、有序性。
而這三大特性,歸根結底,是爲了實現多線程的 數據一致性,使得程序在多線程併發,指令重排序優化的環境中能如預期運行。
原子性即一個操做或者多個操做,要麼所有執行(執行的過程不會被任何因素打斷),要麼就都不執行。即便在多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其餘線程所幹擾。
在 Java 中,爲了保證原子性,提供了兩個高級的字節碼指令 monitorenter
和 monitorexit
。這兩個字節碼,在 Java 中對應的關鍵字就是 synchronized
。
所以,在 Java 中可使用 synchronized
來保證方法和代碼塊內的操做是原子性的。
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。
JMM 是經過 "變量修改後將新值同步回主內存, 變量讀取前從主內存刷新變量值" 這種依賴主內存做爲傳遞媒介的方式來實現的。
Java 實現多線程可見性的方式有:
volatile
synchronized
final
有序性規則表如今如下兩種場景: 線程內和線程間
as-if-serial
)的方式執行,此種方式已經應用於順序編程語言。 synchronized
關鍵字修飾)以及 volatile
字段的操做仍維持相對有序。 在 Java 中,可使用 synchronized
和 volatile
來保證多線程之間操做的有序性。實現方式有所區別:
volatile
關鍵字會禁止指令重排序。 synchronized
關鍵字經過互斥保證同一時刻只容許一條線程操做。 JMM 爲程序中全部的操做定義了一個偏序關係,稱之爲
先行發生原則(Happens-Before)
。先行發生原則很是重要,它是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,咱們能夠經過幾條規則一攬子地解決併發環境下兩個操做間是否可能存在衝突的全部問題。
unLock
操做先行發生於後面對同一個鎖的 lock
操做。 volatile
變量的寫操做先行發生於後面對這個變量的讀操做。 Thread
對象的 start()
方法先行發生於此線程的每一個一個動做。 Thread.join()
方法結束、Thread.isAlive() 的返回值手段檢測到線程已經終止執行。 interrupt()
方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過 Thread.interrupted()
方法檢測到是否有中斷髮生。 finalize()
方法的開始。 Java 中如何保證底層操做的有序性和可見性?能夠經過內存屏障。
內存屏障是被插入兩個 CPU 指令之間的一種指令,用來禁止處理器指令發生重排序(像屏障同樣),從而保障有序性的。另外,爲了達到屏障的效果,它也會使處理器寫入、讀取值以前,將主內存的值寫入高速緩存,清空無效隊列,從而保障可見性。
舉個例子:
Store1;
Store2;
Load1;
StoreLoad; //內存屏障
Store3;
Load2;
Load3;
複製代碼複製代碼
對於上面的一組 CPU 指令(Store 表示寫入指令,Load 表示讀取指令),StoreLoad 屏障以前的 Store 指令沒法與 StoreLoad 屏障以後的 Load 指令進行交換位置,即重排序。可是 StoreLoad 屏障以前和以後的指令是能夠互換位置的,即 Store1 能夠和 Store2 互換,Load2 能夠和 Load3 互換。
常見有 4 種屏障
LoadLoad
屏障 - 對於這樣的語句 Load1; LoadLoad; Load2
,在 Load2 及後續讀取操做要讀取的數據被訪問前,保證 Load1 要讀取的數據被讀取完畢。 StoreStore
屏障 - 對於這樣的語句 Store1; StoreStore; Store2
,在 Store2 及後續寫入操做執行前,保證 Store1 的寫入操做對其它處理器可見。 LoadStore
屏障 - 對於這樣的語句 Load1; LoadStore; Store2
,在 Store2 及後續寫入操做被執行前,保證 Load1 要讀取的數據被讀取完畢。 StoreLoad
屏障 - 對於這樣的語句 Store1; StoreLoad; Load2
,在 Load2 及後續全部讀取操做執行前,保證 Store1 的寫入對全部處理器可見。它的開銷是四種屏障中最大的(沖刷寫緩衝器,清空無效化隊列)。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。 Java 中對內存屏障的使用在通常的代碼中不太容易見到,常見的有 volatile
和 synchronized
關鍵字修飾的代碼塊(後面再展開介紹),還能夠經過 Unsafe
這個類來使用內存屏障。
volatile
是 JVM 提供的 最輕量級的同步機制。
volatile
的中文意思是不穩定的,易變的,用 volatile
修飾變量是爲了保證變量在多線程中的可見性。
volatile
變量具備兩種特性:
這裏的可見性是指當一條線程修改了 volatile 變量的值,新值對於其餘線程來講是能夠當即得知的。而普通變量不能作到這一點,普通變量的值在線程間傳遞均須要經過主內存來完成。
線程寫 volatile 變量的過程:
線程讀 volatile 變量的過程:
注意:保證可見性不等同於 volatile 變量保證併發操做的安全性
在不符合如下兩點的場景中,仍然要經過枷鎖來保證原子性:
- 運算結果並不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值。
- 變量不須要與其餘狀態變量共同參與不變約束。
可是若是多個線程同時把更新後的變量值同時刷新回主內存,可能致使獲得的值不是預期結果:
舉個例子: 定義 volatile int count = 0
,2 個線程同時執行 count++ 操做,每一個線程都執行 500 次,最終結果小於 1000,緣由是每一個線程執行 count++ 須要如下 3 個步驟:
具體一點解釋,禁止重排序的規則以下:
volatile
變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後面的操做可見;在其後面的操做確定尚未進行; volatile
變量訪問的語句放在其後面執行,也不能把 volatile
變量後面的語句放到其前面執行。 普通的變量僅僅會保證該方法的執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,而不能保證賦值操做的順序與程序代碼中的執行順序一致。
舉個例子:
volatile boolean initialized = false;
// 下面代碼線程A中執行
// 讀取配置信息,當讀取完成後將initialized設置爲true以通知其餘線程配置可用
doSomethingReadConfg();
initialized = true;
// 下面代碼線程B中執行
// 等待initialized 爲true,表明線程A已經把配置信息初始化完成
while (!initialized) {
sleep();
}
// 使用線程A初始化好的配置信息
doSomethingWithConfig();
複製代碼複製代碼
上面代碼中若是定義 initialized 變量時沒有使用 volatile 修飾,就有可能會因爲指令重排序的優化,致使線程 A 中最後一句代碼 "initialized = true" 在 「doSomethingReadConfg()」 以前被執行,這樣會致使線程 B 中使用配置信息的代碼就可能出現錯誤,而 volatile 關鍵字就禁止重排序的語義能夠避免此類狀況發生。
具體實現方式是在編譯期生成字節碼時,會在指令序列中增長內存屏障來保證,下面是基於保守策略的 JMM 內存屏障插入策略:
總結起來,就是「一次寫入,處處讀取」,某一線程負責更新變量,其餘線程只讀取變量(不更新變量),並根據變量的新值執行相應邏輯。例如狀態標誌位更新,觀察者模型變量值發佈。
JMM 要求 lock、unlock、read、load、assign、use、store、write 這 8 種操做都具備原子性,可是對於 64 位的數據類型(long 和 double),在模型中特別定義相對寬鬆的規定:容許虛擬機將沒有被 volatile
修飾的 64 位數據的讀寫操做分爲 2 次 32 位的操做來進行,即容許虛擬機可選擇不保證 64 位數據類型的 load、store、read 和 write 這 4 個操做的原子性。因爲這種非原子性,有可能致使其餘線程讀到同步未完成的「32 位的半個變量」的值。
不過實際開發中,Java 內存模型強烈建議虛擬機把 64 位數據的讀寫實現爲具備原子性,目前各類平臺下的商用虛擬機都選擇把 64 位數據的讀寫操做做爲原子操做來對待,所以咱們在編寫代碼時通常不須要把用到的 long 和 double 變量專門聲明爲 volatile。
咱們知道,final 成員變量必須在聲明的時候初始化或者在構造器中初始化,不然就會報編譯錯誤。 final 關鍵字的可見性是指:被 final 修飾的字段在聲明時或者構造器中,一旦初始化完成,那麼在其餘線程無須同步就能正確看見 final 字段的值。這是由於一旦初始化完成,final 變量的值馬上回寫到主內存。