【Java 併發筆記】併發機制底層實現整理
文前說明
作為碼農中的一員,需要不斷的學習,我工作之餘將一些分析總結和學習筆記寫成部落格與大家一起交流,也希望採用這種方式記錄自己的學習之旅。
本文僅供學習交流使用,侵權必刪。
不用於商業目的,轉載請註明出處。
1. 快取一致性問題

硬體記憶體架構
- 現代計算機一般都有 2 個以上 CPU,而且每個 CPU 還有可能包含多個核心。因此,如果應用是多執行緒的話,這些執行緒可能會在各個 CPU 核心中並行執行。
- 在 CPU 內部有一組 CPU 暫存器,也就是 CPU 的儲存器。
- CPU 操作暫存器的速度要比操作計算機主存快的多。
- 在主存和 CPU 暫存器之間還存在一個 CPU 快取,CPU 操作 CPU 快取的速度快於主存但慢於 CPU 暫存器。某些 CPU 可能有多個快取層(一級快取和二級快取)。計算機的主存也稱作 RAM,所有的 CPU 都能夠訪問主存,而且主存比上面提到的快取和暫存器大很多。
- 當一個 CPU 需要訪問主存時,會先讀取一部分主存資料到 CPU 快取,進而在讀取 CPU 快取到暫存器。當 CPU 需要寫資料到主存時,同樣會先 flush 暫存器到
CPU 快取,然後再在某些節點把快取資料 flush 到主存。

快取架構
- 快取大大縮小了高速 CPU 與低速記憶體之間的差距。以三層快取架構為例。
- Core0 與 Core1 命中了記憶體中的同一個地址,那麼各自的 L1 Cache 會快取同一份資料的副本。
- Core0 修改了資料,兩份快取中的資料不同了,Core1 L1 Cache 中的資料相當於失效了。
- 除三級快取外,各廠商實現的硬體架構中還存在多種多樣的快取,都存在類似的可見性問題。例如,暫存器就相當於 CPU 與 L1 Cache 之間的快取。
1.2 MESI 協議
- MESI(Modified Exclusive Shared Or Invalid,快取的四種狀態)協議的基本原理。
- Core0 修改資料 v 後,傳送一個訊號,將 Core1 快取的資料 v 標記為失效,並將修改值寫回記憶體。
- Core0 可能會多次修改資料 v,每次修改都只發送一個訊號(發訊號時會鎖住快取間的匯流排),Core1 快取的資料 v 保持著失效標記。
- Core1 使用資料 v 前,發現快取中的資料 v 已經失效了,得知資料 v 已經被修改,於是重新從其他快取或記憶體中載入資料 v。
- MESI 協議可以解決 CPU 快取層面的一致性問題。

MESI 協議快取狀態
狀態 | 說明 |
---|---|
M(修改,Modified) | 本地處理器已經修改快取行, 即是髒行, 它的內容與記憶體中的內容不一樣. 並且此 cache 只有本地一個拷貝(專有)。 |
E(專有,Exclusive) | 快取行內容和記憶體中的一樣, 而且其它處理器都沒有這行資料。 |
S(共享,Shared) | 快取行內容和記憶體中的一樣, 有可能其它處理器也存在此快取行的拷貝。 |
I(無效,Invalid) | 快取行失效, 不能使用。 |
2. 優化重排序問題
- 在執行程式時,為了提高效能,處理器和編譯器會對指令做重排序。
- 指令級並行的重排序 。如果不存在資料依賴性, 處理器 可以改變語句對應機器指令的執行順序。
- 編譯器優化的重排序 。 編譯器 在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
- 記憶體系統的重排序 。處理器使用 快取和讀/寫緩衝區 ,這使得載入和儲存操作看上去可能是在亂序執行。(導致的可見性問題也可以通過 MESI 協議解決)

重排序
- 重排序不是隨意重排序,它需要滿足以下兩個條件。
資料依賴性
- 如果兩個操作訪問同一個變數,其中一個為寫操作,此時這兩個操作之間存在資料依賴性。編譯器和處理器不會改變存在資料依賴性關係的兩個操作的執行順序,即不會重排序。
as-if-serial
- 所有的動作(Action)都可以為了優化而被重排序,但是必須保證它們重排序後的結果和程式程式碼本身(單執行緒下的執行)的應有結果是一致的,編譯器、runtime 和處理器都必須遵守 as-if-serial 語義。
2.1 指令級並行的重排序(處理器)

指令級並行的重排序
- 只要不影響程式單執行緒、順序執行的結果,就可以對兩個指令重排序。
- 亂序執行技術是處理器為提高運算速度而做出違背程式碼原有順序的優化。
不優化時的執行過程 | 優化時的執行過程 |
---|---|
指令獲取。 | 指令獲取。 |
如果輸入的運算物件是可以獲取的(比如已經存在於暫存器中),這條指令會被髮送到合適的功能單元。如果一個或者更多的運算物件在當前的時鐘週期中是不可獲取的(通常需要從主記憶體獲取),處理器會開始等待直到它們是可以獲取的。 | 指令被髮送到一個指令序列(也稱執行緩衝區或者保留站)中。 |
指令在合適的功能單元中被執行。 | 指令將在序列中等待,直到它的資料運算物件是可以獲取的。然後,指令被允許在先進入的、舊的指令之前離開序列緩衝區。(此處表現為亂序) |
功能單元將運算結果寫回暫存器。 | 指令被分配給一個合適的功能單元並由之執行。 |
結果被放到一個序列中。 | |
僅當所有在該指令之前的指令都將他們的結果寫入暫存器後,這條指令的結果才會被寫入暫存器中。(重整亂序結果) |
2.2 編譯器優化的重排序
- 和處理器亂序執行的目的是一樣的,與其等待阻塞指令(如等待快取刷入)完成,不如先執行其他指令。與處理器亂序執行相比,編譯器重排序能夠完成更大範圍、效果更好的亂序優化。
- 編譯器層面的重排序,自然可以由編譯器控制。使用 volatile 做標記,就可以禁用編譯器層面的重排序。
- JVM 自己維護的 記憶體模型 中也有可見性問題,使用 volatile 做標記,取消 volatile 變數的快取,就解決了 JVM 層面的可見性問題。
3. 記憶體模型
- 可以把記憶體模型理解為在特定操作協議下,對特定的記憶體或快取記憶體進行讀寫訪問的過程抽象。
- 在併發程式設計需要處理的兩個關鍵問題是:執行緒之間如何 通訊 和 執行緒之間如何 同步 。
通訊
- 通訊是指執行緒之間以何種機制來交換資訊。
- 指令式程式設計中,執行緒之間的通訊機制有兩種,是 共享記憶體 和 訊息傳遞 。
- 共享記憶體的併發模型裡,執行緒之間共享程式的公共狀態,執行緒之間通過寫-讀記憶體中的 公共狀態 來 隱式 進行通訊。
- 訊息傳遞的併發模型裡,執行緒之間沒有公共狀態,執行緒之間必須通過明確的 傳送訊息 來 顯式 進行通訊。
同步
-
同步是指程式用於控制不同執行緒之間操作發生相對順序的機制。
- 共享記憶體的併發模型裡,同步是 顯式 進行的。程式設計師必須顯式指定某個方法或某段程式碼需要線上程之間 互斥執行 。
- 訊息傳遞的併發模型裡,由於訊息的傳送必須在訊息的接收之前,因此同步是 隱式 進行的。
-
Java 的併發採用的是 共享記憶體模型 ,執行緒之間的通訊對程式設計師完全透明。
3.1 順序一致性記憶體模型
- 順序一致性記憶體模型有兩大特性。
- 一個執行緒中的所有操作必須按照程式的順序來執行。
- (不管程式是否同步)所有執行緒都只能看到一個單一的操作執行順序。在順序一致性記憶體模型中,每個操作都必須原子執行且立刻對所有執行緒可見。

順序一致性記憶體模型
- 在概念上,順序一致性模型有一個單一的全域性記憶體,這個記憶體通過一個左右擺動的開關可以連線到任意一個執行緒,同時每一個執行緒必須按照程式的順序來執行記憶體讀/寫操作。
- 在任意時間點最多隻能有一個執行緒可以連線到記憶體。
- 當多個執行緒併發執行時,開關裝置能把所有執行緒的所有記憶體讀/寫操作序列化。

一致性模型執行效果
- 假設這兩個執行緒使用監視器鎖來正確同步:A 執行緒的三個操作執行後釋放監視器鎖,隨後 B 執行緒獲取同一個監視器鎖。那麼程式在順序一致性模型中的執行效果。

未同步程式在一致性模型中執行效果
- 未同步程式在順序一致性模型中雖然整體執行順序是無序的,但所有執行緒都只能看到一個一致的整體執行順序。
- 之所以能得到這個保證是因為順序一致性記憶體模型中的每個操作必須立即對任意執行緒可見。
- 在 JMM 中就沒有這個保證。未同步程式在 JMM 中不但整體的執行順序是無序的,而且所有執行緒看到的操作執行順序也可能不一致。比如,在當前執行緒把寫過的資料快取在本地記憶體中,在還沒有重新整理到主記憶體之前,這個寫操作僅對當前執行緒可見。
- 從其他執行緒的角度來觀察,會認為這個寫操作根本還沒有被當前執行緒執行。只有當前執行緒把本地記憶體中寫過的資料重新整理到主記憶體之後,這個寫操作才能對其他執行緒可見。
- 在這種情況下,當前執行緒和其它執行緒看到的操作執行順序將不一致。
- 在順序一致性模型中,所有操作完全按程式的順序執行。而在 JMM 中,臨界區內的程式碼可以重排序(但 JMM 不允許臨界區內的程式碼 " 逸出 " 到臨界區之外,那樣會破壞監視器的語義)。JMM 會在退出臨界區和進入臨界區這兩個關鍵時間點做一些特別處理,使得執行緒在這兩個時間點具有與順序一致性模型相同的記憶體檢視。
- JMM 在具體實現上的基本方針:在不改變(正確同步的)程式執行結果的前提下,儘可能的為編譯器和處理器的優化開啟方便之門。
4. 抽象結構(JMM)
- 不同架構的物理計算機可以有不一樣的記憶體模型,Java 虛擬機器也有自己的記憶體模型。
- Java 虛擬機器規範中試圖定義一種 Java 記憶體模型(Java Memory Model,簡稱 JMM)來遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓 Java 程式在各種平臺下都能達到一致的記憶體訪問效果,不必因為不同平臺上的物理機的記憶體模型的差異,對各平臺定製化開發程式。
- Java 記憶體模型提出目標在於,定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。
- Java 執行緒之間的通訊由 Java 記憶體模型(JMM)控制,JMM 決定一個執行緒對共享變數(例項域、靜態域和陣列)的寫入何時對其它執行緒可見。
- 從抽象的角度來看,JMM 定義了執行緒和主記憶體 Main Memory(堆記憶體)之間的抽象關係:執行緒之間的共享變數儲存在主記憶體中,每個執行緒都有自己的本地記憶體(工作記憶體) Local Memory(只是一個抽象概念,物理上不存在),儲存了該執行緒的共享變數副本。
- 本地記憶體是 JMM 的一個抽象概念,並不真實存在。它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器優化。
- JMM 屬於語言級的記憶體模型,它確保在不同的編譯器和不同的處理器平臺之上,通過插入特定型別的 Memory Barrier (記憶體屏障)來禁止特定型別的編譯器重排序和處理器重排序,為上層提供一致的記憶體可見性保證。

Java 記憶體模型抽象
- 執行緒 A 和執行緒 B 之間需要通訊的話,必須經過兩個步驟:
- 執行緒 A 把本地記憶體(工作記憶體) A 中更新過的共享變數副本重新整理到主記憶體中。
- 執行緒 B 到主記憶體中讀取執行緒 A 之前更新過的共享變數。
- 兩個步驟實質上是執行緒 A 再向執行緒 B 傳送訊息,而這個通訊過程必須經過主記憶體。
- JMM 通過控制主記憶體與每個執行緒的本地記憶體(工作記憶體)之間的互動,來為 Java 程式設計師提供記憶體可見性保證。
4.1 Java 記憶體模型的實現
- 關於主記憶體與本地記憶體(工作記憶體)之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體之類的實現細節。
- Java 記憶體模型中定義了下面 8 種操作來完成。

8 種基本操作
操作 | 作用 |
---|---|
lock (鎖定) | 作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。 |
unlock (解鎖) | 作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。 |
read (讀取) | 作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的 load 動作使用。 |
load (載入) | 作用於工作記憶體的變數,它把 read 操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。 |
use (使用) | 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時就會執行這個操作。 |
assign (賦值) | 作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。 |
store (儲存) | 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後 write 操作使用。 |
write (寫入) | 作用於主記憶體的變數,它把 Store 操作從工作記憶體中得到的變數的值放入主記憶體的變數中。 |
- 記憶體互動基本操作的 3 個特性。
原子性
- 原子性即一個操作或者多個操作,要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。原子是世界上的最小單位,具有不可分割性。
- 在 Java 中,對基本資料型別的變數的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。
可見性
- 可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
- JMM 是通過線上程 A 變數工作記憶體修改後將新值同步回主記憶體,執行緒 B 在變數讀取前從主記憶體重新整理變數值,這種依賴主記憶體作為傳遞媒介的方式來實現可見性。
有序性
-
有序性規則表現在以下兩種場景。
- 執行緒內 ,從某個執行緒的角度看方法的執行,指令會按照一種叫 " 序列 "(as-if-serial)的方式執行,此種方式已經應用於順序程式語言。
- 執行緒間 ,這個執行緒 " 觀察 " 到其他執行緒併發地執行非同步的程式碼時,由於指令重排序優化,任何程式碼都有可能交叉執行。
- 唯一起作用的約束是:對於同步方法,同步塊(synchronized 關鍵字修飾)以及 volatile 欄位的操作仍維持相對有序。
-
Java 記憶體模型的一系列執行規則,都是圍繞原子性、可見性、有序性特徵建立。是為了實現共享變數的在多個執行緒的工作記憶體的資料一致性,多執行緒併發,指令重排序優化的環境中程式能如預期執行。
4.2 happens-before 關係(先行發生原則)
- 從 jdk5 開始,Java 使用新的 JSR-133 記憶體模型,基於 happens-before 的概念來闡述操作之間的記憶體可見性。
- 在 JMM 中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在 happens-before 關係。這裡提到的兩個操作既可以是在一個執行緒之內,也可以是在不同執行緒之間。
- happens-before 關係的分析需要分為單執行緒和多執行緒的情況。
- 單執行緒 下的 happens-before,位元組碼的先後順序天然包含 happens-before 關係:因為單執行緒內共享一份工作記憶體,不存在資料一致性的問題。
- 在程式控制流路徑中靠前的位元組碼 happens-before 靠後的位元組碼,即靠前的位元組碼執行完之後操作結果對靠後的位元組碼可見。然而,這並不意味著前者一定在後者之前執行。實際上,如果後者不依賴前者的執行結果,那麼它們可能會被重排序。
- 多執行緒 下的 happens-before,多執行緒由於每個執行緒有共享變數的副本,如果沒有對共享變數做同步處理,執行緒 1 更新執行操作 A 共享變數的值之後,執行緒 2 開始執行操作 B,此時操作 A 產生的結果對操作 B 不一定可見。
- 單執行緒 下的 happens-before,位元組碼的先後順序天然包含 happens-before 關係:因為單執行緒內共享一份工作記憶體,不存在資料一致性的問題。
- Java 記憶體模型實現了下述支援 happens-before 關係的操作。
規則 | 說明 |
---|---|
程式次序規則 | 一個執行緒內,按照程式碼順序,書寫在前面的操作 happens-before 書寫在後面的操作。 |
鎖定規則 | 一個 unLock 操作 happens-before 後面對同一個鎖的 lock 操作。 |
volatile 變數規則 | 對一個變數的寫操作 happens-before 後面對這個變數的讀操作。 |
傳遞規則 | 如果操作 A happens-before 操作 B,而操作 B 又 happens-before 操作 C,則可以得出操作 A happens-before 操作 C。 |
執行緒啟動規則 | Thread 物件的 start() 方法 happens-before 此執行緒的每個一個動作。 |
執行緒中斷規則 | 對執行緒 interrupt() 方法的呼叫 happens-before 被中斷執行緒的程式碼檢測到中斷事件的發生。 |
執行緒終結規則 | 執行緒中所有的操作都 happens-before 執行緒的終止檢測,我們可以通過 Thread.join() 方法結束、Thread.isAlive() 的返回值手段檢測到執行緒已經終止執行。 |
物件終結規則 | 一個物件的初始化完成 happens-before 它的 finalize() 方法的開始。 |
4.3 Memory Barrier(記憶體屏障)
- 記憶體屏障(Memory Barrier),又稱記憶體柵欄,是一個 CPU 指令。
- 基本記憶體屏障可以分為:LoadLoad 屏障、LoadStore 屏障、StoreStore 屏障和 StoreLoad 屏障。
- 記憶體屏障有兩個作用。
- 阻止屏障兩側的指令重排序,插入一條 Memory Barrier 會告訴編譯器和 CPU,不管什麼指令都不能和這條 Memory Barrier 指令重排序。
- 強制把寫緩衝區/快取記憶體中的髒資料等寫回主記憶體,讓快取中相應的資料失效。如一個 Write-Barrier(寫入屏障)將刷出所有在 Barrier 之前寫入 cache 的資料,因此,任何 CPU 上的執行緒都能讀取到這些資料的最新版本。
- 記憶體屏障阻礙了 CPU 採用優化技術來降低記憶體操作延遲,因此必定會帶來效能損失。

Memory Barrier
屏障型別 | 指令示例 | 說明 |
---|---|---|
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 這個類來使用記憶體屏障。
4.3.1 x86架構的記憶體屏障
- x86架構並沒有實現全部的記憶體屏障。
Store Barrier
- sfence 指令實現了Store Barrier,相當於 StoreStore Barriers。
- 強制所有在 sfence 指令之前的 store 指令,都在該 sfence 指令執行之前被執行,傳送快取失效訊號,並把 store buffer 中的資料刷出到 CPU 的 L1 Cache 中。
- 所有在 sfence 指令之後的 store 指令,都在該 sfence 指令執行之後被執行。即禁止對 sfence 指令前後 store 指令的重排序跨越 sfence 指令,使所有 Store Barrier 之前發生的記憶體更新都是可見的。這裡的 " 可見 ",指修改值可見(記憶體可見性)且操作結果可見(禁用重排序)。
Load Barrier
- lfence 指令實現了 Load Barrier,相當於 LoadLoad Barriers。
- 強制所有在 lfence 指令之後的 load 指令,都在該 lfence 指令執行之後被執行,並且一直等到 load buffer 被該 CPU 讀完才能執行之後的 load 指令(發現快取失效後發起的刷入)。即禁止對 lfence 指令前後 load 指令的重排序跨越 lfence 指令,配合 Store Barrier,使所有 Store Barrier 之前發生的記憶體更新,對 Load Barrier 之後的 load 操作都是可見的。
Full Barrier
- mfence 指令實現了 Full Barrier,相當於 StoreLoad Barriers。
- mfence 指令綜合了 sfence 指令與 lfence 指令的作用,強制所有在 mfence 指令之前的 store/load 指令,都在該 mfence 指令執行之前被執行。
- 所有在 mfence 指令之後的 store/load 指令,都在該 mfence 指令執行之後被執行。即禁止對 mfence 指令前後 store/load 指令的重排序跨越 mfence 指令,使所有 Full Barrier 之前發生的操作,對所有 Full Barrier 之後的操作都是可見的。
4.4 同步規則
- JMM 在執行前面介紹的 8 種基本操作時,為了保證記憶體間資料一致性,JMM 中規定需要滿足以下規則。
規則 | 說明 |
---|---|
規則 1 | 如果要把一個變數從主記憶體中複製到工作記憶體,就需要按順序的執行 read 和 load 操作,如果把變數從工作記憶體中同步回主記憶體中,就要按順序的執行 store 和 write 操作。但 Java 記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。 |
規則 2 | 不允許 read 和 load、store 和 write 操作之一單獨出現。 |
規則 3 | 不允許一個執行緒丟棄它的最近 assign 的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中。 |
規則 4 | 不允許一個執行緒無原因的(沒有發生過任何 assign 操作)把資料從工作記憶體同步回主記憶體中。 |
規則 5 | 一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load 或 assign )的變數。即對一個變數實施 use 和 store 操作之前,必須先執行過了 load 或 assign 操作。 |
規則 6 | 一個變數在同一個時刻只允許一條執行緒對其進行 lock 操作,但 lock 操作可以被同一條執行緒重複執行多次,多次執行 lock 後,只有執行相同次數的 unlock 操作,變數才會被解鎖。所以 lock 和 unlock 必須成對出現。 |
規則 7 | 如果對一個變數執行 lock 操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行 load 或 assign 操作初始化變數的值。 |
規則 8 | 如果一個變數事先沒有被 lock 操作鎖定,則不允許對它執行 unlock 操作;也不允許去 unlock 一個被其他執行緒鎖定的變數。 |
規則 9 | 對一個變數執行 unlock 操作之前,必須先把此變數同步到主記憶體中(執行 store 和 write 操作)。 |
- 規則 1、規則 2,工作記憶體中的共享變數作為主記憶體的副本。
- 主記憶體變數的值同步到工作記憶體需要 read 和 load 一起使用。
- 工作記憶體中的變數的值同步回主記憶體需要 store 和 write 一起使用。
- 這 2 組操作各自都是一個固定的有序搭配,不允許單獨出現。
- 規則 3、規則 4,由於工作記憶體中的共享變數是主記憶體的副本,為保證資料一致性,當工作記憶體中的變數被位元組碼引擎重新賦值,必須同步回主記憶體。
- 如果工作記憶體的變數沒有被更新,不允許無原因同步回主記憶體。
- 規則 5,由於工作記憶體中的共享變數是主記憶體的副本,必須從主記憶體誕生。
- 規則 6、7、8、9,為了併發情況下安全使用變數,執行緒可以基於 lock 操作獨佔主記憶體中的變數,其他執行緒不允許使用或 unlock 該變數,直到變數被執行緒 unlock。
4.5 volatile 關鍵字
- volatile 的語義是 保證可見性 和 禁止進行指令重排序 ,不能確保 原子性 。
保證可見性
- 保證了不同執行緒對該變數操作的記憶體可見性。
- 執行緒對變數進行修改之後,要立刻回寫到主記憶體。
- 執行緒對變數讀取的時候,要從主記憶體中讀,而不是從執行緒的工作記憶體。
禁止進行指令重排序
- 當程式執行到 volatile 變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見。在其後面的操作肯定還沒有進行。
- 在進行指令優化時,不能將在對 volatile 變數訪問的語句放在其後面執行,也不能把 volatile 變數後面的語句放到其前面執行。
- 普通的變數僅僅會保證該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證賦值操作的順序與程式程式碼中的執行順序一致。
4.5.1 volatile 的實現原理

volatile
- 具體實現方式是在編譯期生成位元組碼時,會在指令序列中增加 記憶體屏障 來保證。
- 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障。該屏障除了保證了屏障之前的寫操作和該屏障之後的寫操作不能重排序,還會保證了 volatile 寫操作之前,任何的讀寫操作都會先於 volatile 被提交。
- 在每個 volatile 寫操作的後面插入一個 StoreLoad 屏障。該屏障除了使 volatile 寫操作不會與之後的讀操作重排序外,還會重新整理處理器快取,使 volatile 變數的寫更新對其他執行緒可見。
- 在每個 volatile 讀操作的後面插入一個 LoadLoad 屏障。該屏障除了使 volatile 讀操作不會與之前的寫操作發生重排序外,還會重新整理處理器快取,使 volatile 變數讀取的為最新值。
- 在每個 volatile 讀操作的後面插入一個 LoadStore 屏障。該屏障除了禁止了 volatile 讀操作與其之後的任何寫操作進行重排序,還會重新整理處理器快取,使其他執行緒 volatile 變數的寫更新對 volatile 讀操作的執行緒可見。
4.5.2 volatile 的應用場景
-
使用 volatile 必須具備的條件
- 對變數的寫操作不依賴於當前值。
- 該變數沒有包含在具有其他變數的不變式中。
-
只有在狀態真正獨立於程式內其他內容時才能使用 volatile。
-
模式 #1 狀態標誌
- 也許實現 volatile 變數的規範使用僅僅是使用一個布林狀態標誌,用於指示發生了一個重要的一次性事件,例如完成初始化或請求停機。
volatile boolean shutdownRequested; ...... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } }
- 模式 #2 一次性安全釋出(one-time safe publication)
- 缺乏同步會導致無法實現可見性,這使得確定何時寫入物件引用而不是原始值變得更加困難。在缺乏同步的情況下,可能會遇到某個物件引用的更新值(由另一個執行緒寫入)和該物件狀態的舊值同時存在。(這就是造成著名的雙重檢查鎖定(double-checked-locking)問題的根源,其中物件引用在沒有同步的情況下進行讀操作,產生的問題是您可能會看到一個更新的引用,但是仍然會通過該引用看到不完全構造的物件)。
public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // do lots of stuff theFlooble = new Flooble();// this is the only write to theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // do some stuff... // use the Flooble, but only if it is ready if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } }
- 模式 #3:獨立觀察(independent observation)
- 安全使用 volatile 的另一種簡單模式是定期 釋出 觀察結果供程式內部使用。例如,假設有一種環境感測器能夠感覺環境溫度。一個後臺執行緒可能會每隔幾秒讀取一次該感測器,並更新包含當前文件的 volatile 變數。然後,其他執行緒可以讀取這個變數,從而隨時能夠看到最新的溫度值。
public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } }
- 模式 #4 volatile bean 模式
- 在 volatile bean 模式中,JavaBean 的所有資料成員都是 volatile 型別的,並且 getter 和 setter 方法必須非常普通 —— 除了獲取或設定相應的屬性外,不能包含任何邏輯。此外,對於物件引用的資料成員,引用的物件必須是有效不可變的。(這將禁止具有陣列值的屬性,因為當陣列引用被宣告為 volatile 時,只有引用而不是陣列本身具有 volatile 語義)。對於任何 volatile 變數,不變式或約束都不能包含 JavaBean 屬性。
@ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
- 模式 #5 開銷較低的讀-寫鎖策略
- volatile 的功能還不足以實現計數器。因為 ++x 實際上是三種操作(讀、新增、儲存)的簡單組合,如果多個執行緒湊巧試圖同時對 volatile 計數器執行增量操作,那麼它的更新值有可能會丟失。
- 如果讀操作遠遠超過寫操作,可以結合使用內部鎖和 volatile 變數來減少公共程式碼路徑的開銷。
- 安全的計數器使用 synchronized 確保增量操作是原子的,並使用 volatile 保證當前結果的可見性。如果更新不頻繁的話,該方法可實現更好的效能,因為讀路徑的開銷僅僅涉及 volatile 讀操作,這通常要優於一個無競爭的鎖獲取的開銷。
@ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } }
4.6 final 關鍵字
final 域的重排序規則
- 在建構函式內對一個 final 域的寫入,與隨後把這個被構造物件的引用賦值給一個引用變數,這兩個操作不能重排序。
- 初次讀一個包含 final 域的物件的引用,與隨後初次讀這個 final 域,這兩個操作之間不能重排序。
寫 final 域的重排序規則
- JMM 禁止編譯器把 final 域的寫重排序到建構函式之外。
- final 成員變數必須在宣告的時候初始化或者在構造器中初始化,否則就會報編譯錯誤。
- 被 final 修飾的欄位在宣告時或者構造器中,一旦初始化完成,那麼在其他執行緒無須同步就能正確看見 final 欄位的值。
- 編譯器會在 final 域的寫之後,建構函式 return 之前,插入一個 StoreStore 屏障。這個屏障禁止處理器把 final 域的寫重排序到建構函式之外。
讀 final 域的重排序規則
-
初次讀一個包含 final 域的物件的引用,與隨後初次讀這個 final 域,這兩個操作之間不能重排序。編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障。
-
當建構函式結束時,final 型別的值是被保證其他執行緒訪問該物件時,它們的值是可見的。
-
final 型別的成員變數的值,包括那些用 final 引用指向的 collections 的物件,是讀執行緒安全而無需使用 synchronized 的。
4.6.1 使用 final 的限制條件和侷限性
- 當宣告一個 final 成員時,必須在建構函式退出前設定它的值。
public class MyClass { private final int myField = 1; public MyClass() { ... } }
或者
public class MyClass { private final int myField; public MyClass() { ... myField = 1; ... } }
- 將指向物件的成員宣告為 final 只能將該引用設為不可變的,而非所指的物件。
下面的方法仍然可以修改該 list。
private final List myList = new ArrayList(); myList.add("Hello");
宣告為 final 可以保證如下操作不合法
myList = new ArrayList(); myList = someOtherList;
- 如果一個物件將會在多個執行緒中訪問並且你並沒有將其成員宣告為 final,則必須提供其他方式保證執行緒安全。
- " 其他方式 " 可以包括宣告成員為 volatile,使用 synchronized 或者顯式 Lock 控制所有該成員的訪問。
4.7 synchronized 關鍵字
- synchronized 是 Java 中的關鍵字,是一種同步鎖。
- 修飾一個程式碼塊,被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號 {} 括起來的程式碼,作用的物件是大括號中的物件。一次只有一個執行緒進入該程式碼塊,此時,執行緒獲得的是成員鎖。
- 修飾一個方法,被修飾的方法稱為同步方法,鎖是當前例項物件。執行緒獲得的是成員鎖,即一次只能有一個執行緒進入該方法,其他執行緒要想在此時呼叫該方法,只能排隊等候。
- 修飾一個靜態的方法,其作用的範圍是整個靜態方法,鎖是當前 Class 物件。執行緒獲得的是物件鎖,即一次只能有一個執行緒進入該方法(該類的所有例項),其他執行緒要想在此時呼叫該方法,只能排隊等候。
- 當 synchronized 鎖住一個物件後,別的執行緒如果也想拿到這個物件的鎖,就必須等待這個執行緒執行完成釋放鎖,才能再次給物件加鎖,這樣才達到執行緒同步的目的。即使兩個不同的程式碼段,都要鎖同一個物件,那麼這兩個程式碼段也不能在多執行緒環境下同時執行。
- 在使用 synchronized 關鍵字的時候,能縮小程式碼段的範圍就儘量縮小,能在程式碼段上加同步就不要再整個方法上加同步。
- 無論 synchronized 關鍵字加在方法上還是物件上,它取得的鎖都是物件,而不是把一段程式碼或函式當作鎖。而且同步方法很可能還會被其他執行緒的物件訪問。
- 每個物件只有一個鎖(lock)與之相關聯。
- 實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。
4.8 鎖
-
鎖釋放和獲取的記憶體語義。
- 當執行緒釋放鎖時,JMM 會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體中。
- 當執行緒獲取鎖時,JMM 會把該執行緒對應的本地記憶體置為無效。從而使得被監視器保護的臨界區程式碼必須要從主記憶體中去讀取共享變數。
-
concurrent 包的原始碼實現通用化的實現模式。
- 首先,宣告共享變數為 volatile。
- 然後,使用 CAS 的原子條件更新來實現執行緒之間的同步。
- 同時,配合以 volatile 的讀/寫和 CAS 所具有的 volatile 讀和寫的記憶體語義來實現執行緒之間的通訊。
-
AQS,非阻塞資料結構和原子變數類(java.util.concurrent.atomic 包中的類),這些 concurrent 包中的基礎類都是使用這種模式來實現的,而 concurrent 包中的高層類又是依賴於這些基礎類來實現的。

concurrent 包的原始碼實現
4.9 long 和 double 型變數
- Java 記憶體模型要求 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。
4.10 小結
- JMM 是一個語言級的記憶體模型,處理器記憶體模型是硬體級的記憶體模型,順序一致性記憶體模型是一個理論參考模型。
- 如果完全按照順序一致性模型來實現處理器和 JMM,那麼很多處理器和編譯器優化都要被禁止,這對效能將會有很大的影響。
- 根據不同型別的讀/寫操作組合的執行順序的放鬆,可以把常見處理器的記憶體模型劃分為以下幾種型別。
- 放鬆程式中寫-讀操作的順序。產生 Total Store Ordering 記憶體模型(TSO)。
- 在上面的基礎上,繼續放鬆程式中的寫-寫操作的順序。產生 Partial Store Order 記憶體模型(PSO)。
- 在前面兩條的基礎上,繼續放鬆程式中的讀-寫 和 讀-讀操作的順序。產生 Relaxed Memory Order 記憶體模型(RMO)和 PowerPc 記憶體模型。
- 注意:這裡處理器對讀/寫的放鬆,是以兩個操作之間不存在資料依賴性為前提的。

記憶體模型的強弱對比
- JMM 的設計示意圖。

JMM 的設計示意圖
- JMM 中與在順序一致性記憶體模型中的執行結果的異同。

JMM 中與在順序一致性記憶體模型中的執行結果的異同
參考來源
ofollow,noindex">https://www.jianshu.com/p/64240319ed60
https://www.jianshu.com/p/d3fda02d4cae
https://blog.csdn.net/suifeng3051/article/details/52611310
http://www.cnblogs.com/zhiji6/p/10037690.html