文章目錄
- 1,線程間的同步和通信
- 1.1, 共享內存并發模型 (Shared Memory Model)
- 線程通信機制
- 線程同步機制
- 特點
- 1.2, 消息傳遞并發模型 (Message Passing Model)
- 線程通信機制
- 線程同步機制
- 特點
- 適用場景對比
- 2,Java內存模型JMM
- 2.0,Java內存模型的基礎
- (1)內存屏障
- (2)happens before
- 2.1,內存可見性
- (1)為什么會出現內存可見性問題?
- (2)內存可見性的發生過程
- (3)JMM如何保證內存可見性
- 2.2,JMM與重排序
- (1)指令重排序的類型
- (2)JMM如何限制重排序
- 2.3順序一致性模型
- (1)核心定義
- (2)同步程序的順序一致性效果
- 根據 JMM 的規定,線程對共享變量的所有操作都必須在自己的本地內存中進行,不能直接從主存中讀取。
- Java 運行時內存區域和JMM
- Java 運行時內存區域描述的是在 JVM 運行時,如何將內存劃分為不同的區域,并且每個區域的功能和工作機制。
- Java 內存模型 (JMM) 主要針對的是多線程環境下,如何在主內存與工作內存之間安全地執行操作。它涵蓋的主題包括變量的可見性、指令重排、原子操作等,旨在解決由于多線程并發編程帶來的一些問題。(可見性,有序性,原子性)
- 指令重排是為了提高 CPU 性能,但是可能會導致一些問題,比如多線程環境下的內存可見性問題。
1,線程間的同步和通信
并發編程的線程之間存在兩個問題:
-
線程間如何通信?即:線程之間以何種機制來交換信息
-
線程間如何同步?即:線程以何種機制來控制不同線程間發生的相對順序
有兩種并發模型可以解決這兩個問題:
- 消息傳遞并發模型
- 共享內存并發模型
1.1, 共享內存并發模型 (Shared Memory Model)
Java主要采用這種模型
線程通信機制
- 通過共享內存進行通信
- 線程之間共享程序的公共狀態(變量、對象等)
- 線程通過讀寫共享內存中的變量來隱式通信
線程同步機制
- 使用顯式同步原語控制執行順序
- 主要同步手段:
- 鎖(synchronized, Lock)
- volatile變量
- 原子變量(AtomicInteger等)
- 內存屏障
特點
- 通信是隱式的(通過內存訪問)
- 需要程序員顯式控制同步
- 容易出現競態條件、死鎖等問題
1.2, 消息傳遞并發模型 (Message Passing Model)
如Go語言的channel、Actor模型
線程通信機制
- 通過發送和接收消息進行顯式通信
- 線程/進程間沒有共享狀態
- 消息通道是唯一的通信媒介
線程同步機制
- 通信本身就是同步的(發送和接收操作)
- 常見實現方式:
- 同步消息傳遞(發送者阻塞直到消息被接收)
- 異步消息傳遞+消息隊列
- CSP(Communicating Sequential Processes)模型
特點
- 通信是顯式的(明確的send/receive操作)
- 同步內建于通信機制中
- 避免了共享內存帶來的許多問題
適用場景對比
場景 | 推薦模型 | 原因 |
---|---|---|
分布式系統 | 消息傳遞 | 天然適合網絡通信 |
單機高并發 | 共享內存 | 性能更高 |
簡單并發任務 | 消息傳遞 | 更易實現和維護 |
復雜數據共享 | 共享內存 | 更高效的數據訪問 |
容錯系統 | 消息傳遞 | 更好的隔離性和恢復能力 |
實時系統 | 共享內存 | 更低延遲 |
2,Java內存模型JMM
Java內存模型(Java Memory Model, JMM)是Java虛擬機規范中定義的一種內存訪問規范,它是一種抽象概念,包含緩存、寫緩沖區、寄存器等。它規定了多線程環境下如何正確地訪問共享變量,以及線程之間如何通過內存進行通信。即解決上述的“線程間如何通信”和“線程間如何同步兩個問題”。保證多線程環境下的可見性、有序性和原子性。
JMM解決的三大問題
問題類型 | 描述 | JMM解決方案 |
---|---|---|
可見性 | 一個線程修改共享變量后其他線程立即可見 | volatile、synchronized、final |
有序性 | 指令執行順序與代碼順序一致 | happens-before、內存屏障 |
原子性 | 操作不可中斷 | synchronized、原子類 |
2.0,Java內存模型的基礎
(1)內存屏障
屏障類型 | 作用 |
---|---|
LoadLoad | 禁止 Load1 和 Load2 重排序 |
StoreStore | 禁止 Store1 和 Store2 重排序 |
LoadStore | 禁止 Load 和后續 Store 重排序 |
StoreLoad | 禁止 Store 和后續 Load 重排序 |
eg:LoadLoad屏障
確保 Load1
先于 Load2
執行,防止讀操作重排序。
StoreLoad 屏障(全能屏障)
- 作用:
- 禁止
Store
和后續Load
重排序。 - 強制刷新所有寫操作到主內存,并 使其他 CPU 緩存失效。
- 禁止
- 開銷最大,但能保證最強的內存一致性。‘
(2)happens before
Happens-Before 是 Java 內存模型(JMM)的核心概念,它定義了多線程環境下操作之間的可見性保證和執行順序約束,使開發者能夠在不深入理解底層內存屏障的情況下編寫正確的并發程序。
Happens-Before 描述的是兩個操作之間的偏序關系:
- 如果操作 A happens-before 操作 B,那么:
- A 的執行結果對 B 可見
- A 的代碼順序在 B 之前
📌 注意:Happens-Before 并不一定代表時間上的先后,而是可見性保證!
happens-before的六大規則
- 程序順序規則:同一線程中的操作,按照代碼順序 happens-before。
- 監視器鎖規則:解鎖(unlock) happens-before 后續的加鎖(lock)。
- volatile 變量規則:volatile 寫 happens-before 后續的 volatile 讀。
- 線程啟動規則:
Thread.start()
happens-before 該線程的所有操作。 - 線程終止規則:線程的所有操作 happens-before 其他線程檢測到它終止(如
t.join()
)。 - 傳遞性規則:如果 A happens-before B,且 B happens-before C,則 A happens-before C。
2.1,內存可見性
內存可見性(Memory Visibility)是多線程編程中的一個核心概念,指的是當一個線程修改了共享變量的值后,其他線程能否立即看到這個修改。如果修改后的值不能及時被其他線程觀察到,就會導致內存可見性問題,從而引發程序邏輯錯誤。
什么是共享變量共享變量是指在多線程環境下可以被多個線程共同訪問和修改的變量。對于每一個線程來說,棧都是私有的,而堆是共有的。也就是說,在棧中的變量(局部變量、方法定義的參數、異常處理的參數)不會在線程之間共享,也就不會有內存可見性的問題,也不受內存模型的影響。而在堆中的變量是共享的,一般稱之為共享變量。所以,內存可見性針對的是堆中的共享變量。
(1)為什么會出現內存可見性問題?
現代計算機和 JVM 為了提高性能,會采用以下優化策略,導致內存可見性問題:
(1) CPU 緩存架構
- CPU 不會直接讀寫主內存(RAM),而是通過**多級緩存(L1/L2/L3 Cache)**來提高訪問速度。
- 每個 CPU 核心有自己的緩存,線程運行時可能只更新自己的緩存,而不會立即同步到主內存。
- 因此,一個線程的修改可能對其他線程不可見。
(2) 指令重排序(Reordering)
-
編譯器優化:JIT 編譯器可能會調整指令順序以提高性能。
-
CPU 亂序執行:CPU 可能會改變指令的執行順序(只要不影響單線程語義)。
-
這可能導致線程 A 的修改操作被延遲或亂序執行,導致線程 B 看到的數據不一致。
public class ReorderingProblem {
private static int x = 0;
private static int y = 0;
private static boolean ready = false;public static void main(String[] args) {Thread writer = new Thread(() -> {x = 1;y = 2;ready = true; // 可能被重排序到前面});Thread reader = new Thread(() -> {while (!ready); // 等待ready=trueSystem.out.println("x=" + x + ", y=" + y); // 可能輸出x=0, y=2});writer.start();reader.start(); }
}
由于指令重排序問題,可能會ready = true
可能先于 x = 1
執行,導致 reader
線程看到 x=0
,但 y=2
。
(3) 工作內存(Working Memory)抽象
- JMM(Java 內存模型)規定,每個線程有自己的工作內存(可以理解為 CPU 緩存 + 寄存器 + 寫緩沖區)。
- 線程操作共享變量時,先在工作內存中修改,再同步回主內存,這可能導致其他線程看不到最新值。
(2)內存可見性的發生過程
從圖中可以看出:
- 所有的共享變量都存在主存中。
- 每個線程都保存了一份該線程使用到的共享變量的副本。
- 如果線程 A 與線程 B 之間要通信的話,必須經歷下面 2 個步驟:
- 線程 A 將本地內存 A 中更新過的共享變量刷新到主存中去。
- 線程 B 到主存中去讀取線程 A 之前已經更新過的共享變量。
所以,線程 A 無法直接訪問線程 B 的工作內存,線程間通信必須經過主存。
注意,根據 JMM 的規定,線程對共享變量的所有操作都必須在自己的本地內存中進行,不能直接從主存中讀取。
所以線程 B 并不是直接去主存中讀取共享變量的值,而是先在本地內存 B 中找到這個共享變量,發現這個共享變量已經被更新了,然后本地內存 B 去主存中讀取這個共享變量的新值,并拷貝到本地內存 B 中,最后線程 B 再讀取本地內存 B 中的新值。
(3)JMM如何保證內存可見性
Java內存模型(JMM)的核心作用之一就是解決"如何知道共享變量被其他線程更新了"這個問題。
JMM 通過控制主存與每個線程的本地內存之間的交互,來提供內存可見性保證。
Java 中的 volatile 關鍵字可以保證多線程操作共享變量的可見性以及禁止指令重排序,synchronized 關鍵字不僅保證可見性,同時也保證了原子性(互斥性)。
在更底層,JMM 通過內存屏障來實現內存的可見性以及禁止重排序。為了程序員更方便地理解,設計者提出了 happens-before 的概念,它更加簡單易懂,從而避免了程序員為了理解內存可見性而去學習復雜的重排序規則,以及這些規則的具體實現方法。
2.2,JMM與重排序
Java內存模型(JMM)的一個重要方面就是管理指令重排序(Reordering),它定義了在多線程環境下哪些重排序是被允許的,哪些是被禁止的。理解這一點對編寫正確的并發程序至關重要。
(1)指令重排序的類型
- 編譯器優化的重排序
編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序
- 指令級并行的重排序
現代處理器采用指令級并行技術(ILP)來將多條指令重疊執行
- 內存系統的重排序
由于處理器使用緩存和讀寫緩沖區,使得加載和存儲操作看上去可能是在亂序執行
(2)JMM如何限制重排序
JMM通過以下幾種機制來限制重排序:
- happens-before規則
定義了一系列天然的happens-before關系,在這些關系下禁止重排序:
- 程序順序規則
- 監視器鎖規則
- volatile變量規則
- 線程啟動/終止規則
- 傳遞性規則
- 內存屏障(Memory Barrier)
JMM在關鍵位置插入內存屏障指令來禁止特定類型的重排序:
屏障類型 | 作用 |
---|---|
LoadLoad | 禁止Load1與Load2重排序 |
StoreStore | 禁止Store1與Store2重排序 |
LoadStore | 禁止Load與后續Store重排序 |
StoreLoad | 全能屏障,禁止Store與后續Load重排序(開銷最大) |
- 特殊關鍵字語義
- volatile:禁止與相鄰指令重排序
- final:保證正確構造后的對象對所有線程可見
- synchronized:進入/退出時隱含內存屏障
2.3順序一致性模型
(1)核心定義
順序一致性模型必須滿足兩個基本條件:
- 程序順序保留:每個線程內部的操作必須按照該線程的程序代碼順序執行。(不允許重排序)
- 全局內存順序:所有線程看到的整個系統的操作執行順序必須一致
順序一致性模型雖然理論上完美,但硬件上難以實現,但Java等語言可以提供近似保證。
(2)同步程序的順序一致性效果
在并發編程中,通過同步機制可以使程序表現出順序一致性的內存效果,即使底層硬件和編譯器可能進行各種優化。