本文分為四個部分來講解:
java內存模型的基礎, 主要介紹內存模型相關的基本概念;Java內存模型中的順序一致性, 主要介紹重排序與順序一致性內存模型;同步原語, 主要介紹三個同步原語(synchronized
, volatile
, final
)的內存語義及重排序規則在處理器中的實現;Java內存模型的設計, 主要介紹Java內存模型的設計原理, 及其與處理器內存模型和順序一致性內存模型的關系;線程通信機制主要有兩種: 共享內存和消息傳遞. Java的并發采用的是共享內存模型.
在Java中, 所有實例域, 靜態域和數組元素都存儲在堆內存中, 堆內存在線程之間共享. 局部變量, 方法定義參數, 和異常處理參數不會在線程之間共享, 它們不會有內存可見性問題, 也不受內存模型的影響.
JMM定義了線程和主內存(Main Memory)之間的抽象關系, 屬于語言級的內存模型:
線程之間的共享變量存儲在主內存中, 每個線程又有一個私有的本地內存(Local Memory, 實際上就是Java虛擬機棧, 寄存器, 處理器高速緩存等), 本地內存中存儲了該線程已操作過的共享變量的副本. 本地內存是JMM的一個抽象概念, 并不真實存在, 因為它涵蓋了緩存, 寫緩沖區, 寄存器及其他硬件和編譯器的諸多優化的集合.
如果線程A和線程B要通信的話, 必須要經歷下面兩個步驟:
線程A把本地內存更新過的共享變量刷新到主內存中;線程B到主內存去讀取線程A之前已更新過的共享變量.可以看出, JMM通過控制主內存和每個線程的本地內存(包含緩存, 寄存器等等)之間的交互, 來為Java程序提供內存可見性的保證.
重排序主要是為了提高性能, 通常分為三種:
編譯器優化的重排序. 原則是在不改變單線程程序語義的前提下, 重新安排語句的執行順序;指令級并行的重排序. 在不存在數據依賴性的時候, 處理器可以改變語句對應的機器指令的執行順序, 甚至并行執行指令;內存系統的重排序. 由于處理器使用高速緩存和讀/寫緩沖區, 這使得加載和存儲操作看上去可能是在亂序執行.Java從源代碼到最終執行的指令序列, 會依次進行以上三種重排序. 其中1屬于編譯器重排序, 2和3屬于處理器重排序. 重排序會導致內存可見性的問題. JMM通過設定重排序規則, 禁止特定的編譯器重排序, 對于處理器重排序, 則是通過插入特定類型的內存屏障(Memory Barriers)指令, 來禁止特定類型的處理器重排序, 以確保在不同編譯器和處理器平臺下, 始終能為程序員提供一致的內存可見性保證.
注意: 內存屏障要特別注意Store類型的屏障, 每個Store類型的屏障都對應著將線程私有的寫緩沖寫回到主存的操作, 也就是實現線程間可見性的操作
內存屏障實際上是通過限制單線程內指令的重排序來作用的.
JMM將內存屏障指令分為4種類型:
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad Barriers | Load1; LoadLoad; Load2 | 確保Load1數據的裝載先于Load2指令的裝載(load2的裝載是本線程內部的狀態,其他線程的決定不了) |
StoreStore Barriers | Store1; StoreStore; Store2 | 確保Store1數據對其它處理器可見(將Store1及之前的Store操作數據刷入主內存中)先于Store2的存儲(store2的存儲是本線程內部的存儲, 其他線程的存儲決定不了) |
LoadStore Barriers | Load1; LoadStore; Store2 | 確保Load1數據的裝載先于Store2的存儲(store2的存儲是本線程內部的存儲, 其他線程的存儲決定不了) |
StoreLoad Barriers | Store1; StoreLoad; Load2 | 確保Store1數據對其它處理器可見(將Store1及之前的Store操作刷入主內存中)先于Load2的裝載(load2的裝載發生在線程私有內存內部) |
其中, StoreLoad屏障是一個全能屏障, 因為它包含了其他所有屏障的效果, 但是開銷大, 因為要把寫緩沖區的所有數據全部刷新到內存中.
在JMM中, 如果一個操作執行的結果要對另一個操作可見(通常指的是數據依賴性), 那么這兩個操作之間必須要存在happens-before關系. 主要有以下規則:
程序順序規則: 單線程中的某操作, happens-before于對其有數據依賴性的操作;監視器鎖規則: 對一個鎖的解鎖, happens-before于隨后對這個鎖的加鎖;volatile
變量規則: 對一個volatile
域的寫, happens-before于后續對這個域的讀(實質上是通過緩存鎖定的LOCK信號來實現的); 使所有處理器的相應地址的緩存行失效, 強制重新從共享主存中讀取. 而且不允許兩個線程同時更改同一個緩存行傳遞性: A happens-before B happens-before C, 則 A happens-before C重排序遵守一個統一的原則, 就是讓重排序后的程序至少能夠在單線程的情況下正確運行(意思是在單線程下和重排序前的運行結果相同).
即所有操作具有全序關系, 是一個理想化的模型. 但是JMM天然并不能保證順序一致性, 需要通過同步原語(Synchronized
, volatile
, final
)來輔助完成.
volatile
作用于一個filed上, 能夠確保它的可見性. 例如, 現在有一個filed名為l
, 我們定義PRivate volatile long l
, 就相當于定義:
從內存語義的角度來說, volatile
的寫和鎖的釋放有相同的內存語義; volatile
的讀與鎖的獲取有相同的語義.
volatile
底層實際上是通過內存屏障的方式來確保了可見性, 以下是volatile
附近的內存屏障的情況:
volatile
寫操作的前面插入一個StoreStore屏障;在每個volatile
寫操作的后面插入一個StoreLoad屏障;在每個volatile
讀操作后面插入一個LoadLoad屏障;在每個volatile
讀操作后面插入一個LoadStore屏障;實際使用中volatile
常用做if
或者循環的標識位.
定義成volatile
的變量, 能夠在線程間保持可見性, 能夠被多線程同時讀(注意: 內存屏障只是限制了單線程內的語句排序), 但是同時只能被一個線程寫.
當線程釋放鎖時, JMM會把該線程對應的本地內存中的共享變量刷新到主內存中去; 當線程獲取鎖時, JMM會把該線程對應的本地內存置為無效, 臨界區代碼必須從主內存重新讀取共享變量;
在底層的實現上
在鎖的釋放上, 公平鎖和非公平鎖最后都需要寫一個volatile
變量state
;在鎖的獲取時, 公平鎖會讀volatile
變量, 非公平鎖會用CAS更新volatile
變量.所以鎖的釋放與volatile
的寫, 鎖的獲取同時具有volatile
讀寫的語義.
concurrent包的基礎就是volatile
變量的讀/寫, 以及CAS. CAS兼具volatile
變量讀寫的內存語義
final
域的寫之后, 會插入一個StoreStore屏障final
域的讀之前, 會插入一個LoadLoad屏障只要被構造的對象的引用在構造函數中沒有逸出, 那么基于上述兩條規則, 就不需要使用同步, 就可以保證任意線程都能看到這個final
域在構造函數中被初始化之后的值. 如果逸出了, 那么可能會引起重排序, 導致引用在final
域初始化之前被其他線程獲取, 導致獲得未經初始化的final
域的值.
最實用的三種happens-before
volatile
寫, happens-before后續volatile
讀;以下是一個例子:
/*** 下面一段語句, 能夠保證1 happens before 4, 也就是無論運行多少次, 結果都輸出100** <p>所以結論是, volatile變量非常適合作為循環的標識位.** Created by yihao.cong@Outlook.com on 16-11-4.*/public class VolatileHappensBefore {private volatile static boolean ready = true;private static int number = 1;private static class ReaderThread extends Thread { @Override public void run() { // 3. 子線程讀volatile變量 while (VolatileHappensBefore.ready) { // 這里是LoadLoad+LoadStore屏障 } // 這里是LoadLoad+LoadStore屏障 // 4. 子線程讀共享變量 out.println(VolatileHappensBefore.number); }}public static void main(String[] args) throws InterruptedException { ReaderThread readerThread = new ReaderThread(); readerThread.start(); Thread.sleep(100); /*下面語句復現的是volatile寫讀的happens-before規則*/ // 1. 主線程修改共享變量 VolatileHappensBefore.number = 100; // 這里是StoreStore屏障 // 2. 主線程寫volatile變量 VolatileHappensBefore.ready = false; // 這里是StoreLoad屏障 // 如此一來能夠保證只要volatile變量的修改能夠讀到, 那么之前的修改一定能夠被讀到}}start()
規則: 如果線程A執行操作ThreadB.start()
, 那么線程AThreadB.start()
操作happens-before線程B中的任何操作;join()
規則: 如果線程A執行操作ThreadB.join()
并成功返回, 那么線程B中的任意操作happens-before與線程A在ThreadB.join()
操作的成功返回.雙重檢查鎖定其實是錯誤的, 因為可能一個實例還沒有被完全初始化, 就返回了引用. 導致外層的檢查失效, 使得其他線程獲得一個不完整的對象引用.
替代方案1: 使用volatile
關鍵字修飾單例對象, 確保可見性, 不會讓寫了一半的對象被其他線程讀到; 替代方案2: 基于類的初始化方案; 替代方案3(推薦): 使用enum進行單例的初始化;
volatile
的讀寫語義, 也就是之前之后的代碼都不能重排序. 底層是通過一個lock指令, 進行緩存鎖定, 確保讀-改-寫操作的原子性.緩存一致性和緩存鎖定說的是同一件事, 都是lock指令造成的緩存鎖定(或者說獨占僅那一個地址的主存和緩存).只有volatile
寫操作或者是CAS(一種內置的復合操作)才會觸發lock新聞熱點
疑難解答