介紹
JAVA 內存模型 (JMM) 是 Java 并發性的基石。它定義了線程如何通過內存進行交互以及對內存操作強制執行哪些規則。對于編寫多線程應用程序的開發人員來說,了解 JMM 對于創建高效、無錯誤的程序至關重要。
在這篇文章中,我們將深入研究 JMM 并為開發人員揭開其復雜性。
了解 Java 內存模型
Java 內存模型解釋
Java 內存模型 (JMM) 作為抽象層,規定 Java 程序如何與內存交互,尤其是在多線程環境中。它是 Java 語言規范的一部分,描述了線程和主內存如何通信。
JMM 解決了并發執行帶來的挑戰,例如緩存一致性、內存一致性錯誤、線程爭用以及編譯器和處理器的指令重新排序。通過設置一個定義和預測并發程序行為的框架,確保跨不同平臺和 CPU 與內存的可預測且統一的交互。
線程和主內存交互
在Java中,程序創建的每個線程都有自己的堆棧,其中存儲局部變量和調用信息。然而,線程并不是孤立的;他們經常需要通信、共享對象和變量。這種通信通過主內存進行,主內存保存堆和方法區域。
JMM 描述了一個線程對共享數據(存儲在堆或方法區域中)所做的更改如何以及何時對其他線程可見。這里的主要挑戰是確保線程具有共享數據的最新視圖,出于性能原因,這些數據可能會本地緩存在線程的堆棧中。
內存一致性錯誤
當不同線程對相同數據的視圖不一致時,就會出現內存一致性錯誤。如果沒有適當的內存模型,由于線程調度和可以重新排序指令的編譯器優化的不可預測性,構建線程如何通過內存交互的模型幾乎是不可能的。
JMM 通過提供一組稱為“hAppens-before”的規則來幫助防止這些錯誤,這些規則規定了內存操作(例如讀取和寫入)的排序方式。
可見性和排序
可見性和排序是 JMM 提出的兩個主要概念:
- 可見性是指線程查看多個線程共享的變量的最新值的能力。如果沒有顯式同步,就無法保證一個線程會看到另一個線程已修改的變量的最新值。
- 順序是指變量發生更改的順序。除非使用顯式同步結構,否則 JMM 不會強制跨線程執行嚴格的操作順序。
可見性和順序對于創建線程安全應用程序都至關重要。如果一個線程更新的值對于應該對該更新值執行操作的另一線程不可見,則程序的行為可能會不可預測。類似地,如果操作不按順序執行,則可能會導致線程作用于過時的數據。
happens-before
Java 內存模型的核心是“happens-before”關系。這個原則就是Java中保證內存一致性的規則手冊。“happens-before”關系提供了多線程環境中變量操作(讀取和寫入)的部分順序。
以下是一些構成 JMM 內線程交互基礎的關鍵happens-before規則:
- 程序順序規則:線程中的每個操作發生在該線程中按程序順序出現的每個操作之前。
- 監視器鎖規則:監視器鎖上的解鎖發生在同一監視器鎖上的每個后續鎖定之前。
- volatile變量規則:對volatile字段的寫入發生在同一字段的每次后續讀取之前。
- 線程啟動規則:對線程上的 Thread.start 的調用發生在已啟動線程中的任何操作之前。
- 線程終止規則:線程中的任何操作都發生在任何其他線程檢測到該線程已終止之前,無論是從 Thread.join 成功返回還是 Thread.isAlive 返回 false。
- 中斷規則:一個線程在另一個線程上調用中斷發生在被中斷線程檢測到中斷之前(通過拋出 InterruptedException,或者通過調用 isInterrupted 或 Interrupted)。
- 傳遞性:如果 A 發生在 B 之前,并且 B 發生在 C 之前,則 A 發生在 C 之前。
理解和應用這些規則可確保程序在并發環境中的行為可預測。這些規則是避免內存一致性錯誤、確保可見性和維護操作正確順序的關鍵。
Java 內存模型應用
了解 JMM 的細節使開發人員能夠編寫安全且可擴展的并發應用程序。同步原語(synchronized、volatile等)、原子變量、并發集合的正確使用都植根于JMM。例如,了解volatile變量提供的保證有助于防止過度使用同步,從而提高應用程序的性能和可擴展性。
此外,在 JMM 的上下文中,我們還考慮“as-if-serial”語義,它保證單個線程中的執行行為就像所有操作都按照它們在程序中出現的順序執行一樣 — 即使編譯器實際上可能會在幕后重新排序指令。
Java 內存模型組件
Java 內存模型 (JMM) 是 Java 并發框架的基石,定義了 Java 線程和內存之間的交互。它指定一個線程所做的更改如何以及何時對其他線程可見,從而確保并發執行的可預測性。讓我們檢查一下構成 JMM 的關鍵組件。
共享變量
在Java中,被多個線程訪問的變量是共享變量。這些變量存儲在堆中,堆是內存的共享區域。如果處理不當,共享變量可能會成為內存一致性錯誤的根源。JMM 控制這些共享變量的更改如何在線程內存和主內存之間傳播。
volatile變量
Java中的關鍵字volatile用于將Java變量標記為“正在存儲在主存中”。更準確地說,這意味著對 volatile變量的每次讀取都將從主內存中讀取,而不是從線程的本地緩存中讀取,并且對volatile變量的每次寫入都將寫入主內存,而不僅僅是線程的本地緩存。
public class SharedObject {
private volatile int sharedVariable;
public void updateValue(int newValue) {
sharedVariable = newValue;
}
public int getValue() {
return sharedVariable;
}
}
volatile 關鍵字保證一個線程中所做的更改對另一個線程的可見性。它是同步的輕量級替代方案,盡管它不提供原子性或互斥性。
同步塊
同步是Java中確保線程安全的主要工具之一。同步塊或方法一次只允許一個線程執行一段代碼,確保只有一個線程可以訪問正在同步的資源。
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized 關鍵字確保一個線程所做的更改對其他線程可見,并且還可以防止多個線程同時執行代碼塊。
Final
在 Java 中,final 關鍵字可用于將字段標記為不可變。一旦最終字段被初始化,就不能更改。這種不變性提供了固有的線程安全性,因為無需擔心多個線程修改該值。 JMM 保證設置最終字段的構造函數的效果對于獲得該對象引用的任何線程都是可見的。
public class ImmutableValue {
private final int value;
public ImmutableValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
在這種情況下,一旦創建了 ImmutableValue 實例,value 字段就無法更改,因此不需要進一步同步。
Happens-Before規則
使用 Java 內存模型
同步訪問共享數據
使用 Java 內存模型時,最常見的任務是同步對共享數據的訪問。這確保一次只有一個線程可以訪問代碼的關鍵部分,從而降低內存一致性錯誤的風險。 Synchronized關鍵字可用于鎖定一個對象,以便同一時間只有一個線程可以訪問同步代碼塊或方法。
這是一個使用同步的示例:
public class Account {
private int balance;
public synchronized void deposit(int amount) {
balance += amount;
}
public synchronized int getBalance() {
return balance;
}
}
在上面的示例中,deposit 方法和 getBalance 方法在 Account 類的實例上同步。這意味著,如果一個線程正在執行 Deposit 方法,則在第一個線程退出同步塊之前,其他線程都無法執行 Deposit 或 getBalance。
volatile變量的可見性
volatile變量是使用 JMM 的另一個關鍵方面。當一個字段被聲明為volatile時,編譯器和運行時會被通知該變量是共享的,并且對該變量的操作不應與其他內存操作重新排序。volatile變量可用于確保一個線程所做的更改對其他線程的可見性。
public class Flag {
private volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// perform work
}
}
}
在此示例中,shutdownRequested 標志是volatile的,這可確保 shutdown 方法對 shutdownRequested 所做的更改對于正在檢查該值的任何其他線程立即可見。
原子變量的并發邏輯
Java 在
java.util.concurrent.atomic 包中提供了一組原子變量(例如 AtomicInteger、AtomicLong、AtomicBoolean 等),它們使用高效的機器級并發構造。
這些可用于在不使用同步的情況下安全地對單個變量執行原子操作。
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
原子變量通常是從多個線程訪問的計數器和標志的更好替代方案。
數據共享的并發集合
為了在線程之間共享數據集合,Java 提供了線程安全的變體,例如 ConcurrentHashMap、CopyOnWriteArrayList 和 BlockingQueue。這些集合負責內部同步,并提供比同步標準集合更高的并發性能。
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentCache {
private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
public void putInCache(String key, Object value) {
cache.put(key, value);
}
public Object getFromCache(String key) {
return cache.get(key);
}
}
使用并發集合可以顯著簡化同步訪問集合數據的任務。
處理線程干擾和內存一致性錯誤 線程干擾和內存一致性錯誤是開發人員在處理共享數據時面臨的兩個主要問題。為了避免這些問題,必須了解先發生關系并正確同步對共享變量的訪問。使用同步、volatile變量、原子變量和并發集合可以緩解這些問題。
JMM常見問題及解決方案
線程干擾
問題:當多個線程對共享數據進行操作時,一個線程的操作可能會干擾另一個線程的操作,從而導致錯誤的結果。
解決方案:使用同步機制(如同步塊、
java.util.concurrent.locks 中的鎖或原子變量)來確保一次只有一個線程可以訪問數據。
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
內存一致性錯誤
問題:一個線程對共享變量所做的更改可能對其他線程不可見,從而導致內存一致性錯誤。
解決方案:使用同步塊、易失性變量或不可變對象的最終字段建立happens-before關系。
public class SharedFlag {
private volatile boolean flag = false;
public void setFlag() {
this.flag = true;
}
public boolean checkFlag() {
return flag;
}
}
死鎖
問題:當兩個或多個線程永久阻塞,每個線程都等待另一個線程釋放鎖時,就會發生死鎖。
解決方案:避免死鎖的一種常見策略是對鎖進行排序,并始終以相同的預定義順序獲取多個鎖。
鎖饑餓
問題:當一個或多個線程永遠被拒絕訪問共享資源或鎖時,就會發生饑餓,這通常是因為其他線程占用了資源。
解決方案:使用公平鎖(例如將公平參數設置為 true 的 ReentrantLock)或其他機制來確保所有線程都有機會執行。
import java.util.concurrent.locks.ReentrantLock;
public class FAIrLockExample {
private final ReentrantLock lock = new ReentrantLock(true);
public void fairLockMethod() {
lock.lock();
try {
// 訪問受該鎖保護的資源
} finally {
lock.unlock();
}
}
}
活鎖
問題:活鎖是一種線程未被阻塞的情況——它們只是太忙于相互響應而無法恢復工作。
解決方案:檢測活鎖情況并實施退避策略,讓線程有機會逃脫活鎖狀態。
假共享
問題:當不同處理器上的線程修改駐留在同一緩存行上的變量時,會發生錯誤共享,從而導致不必要的緩存刷新和失效。
解決方案:一種解決方案是填充數據結構,以確保常用訪問的共享變量不共享緩存行。
共享對象的可見性
問題:由于緩存或重新排序,線程可能看不到對象引用或基元的最新值。
解決方案:對不涉及復合操作的簡單標志和引用使用 volatile,確保對變量的寫入立即跨線程反映。
非原子復合操作
問題:讀取-修改-寫入操作(例如遞增計數器)不是原子操作,并且在多個線程訪問時可能會導致狀態不一致。
解決方案:使用
java.util.concurrent.atomic 包中的原子類或同步復合操作。
import java.util.concurrent.atomic.AtomicInteger;
public class SafeCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
總結
Java 內存模型是 Java 的一個復雜部分,需要深入理解才能編寫正確且高效的并發程序。開發人員應努力深入理解 JMM,以避免并發問題并構建健壯的應用程序。通過遵循最佳實踐并理解模型的核心組件和原則,我們可以利發揮出Java 并發編程的真正威力。