JavaEE:多線程進階
- 一、對比不同鎖策略之間的應用場景及其區別
- 1. 悲觀鎖 和 樂觀鎖
- 1.1 定義和原理
- 1.2 應用場景
- 1.3 示例代碼
- 2. 重量級鎖 和 輕量級鎖
- 2.1 定義和原理
- 2.2 應用場景
- 2.3 示例代碼
- 3. 掛起等待鎖 和 自旋鎖
- 3.1 定義和原理
- 3.2 應用場景
- 3.3 示例代碼
- 4. 幾對鎖之間的聯系
- 5. 總結
- 二、synchronized 的優化策略
- 1. 鎖升級(Lazy Initialization)
- 1.1 定義和原理
- 1.2 鎖的狀態轉換圖
- 1.3 應用場景
- 1.4 示例代碼
- 2. 鎖消除(Lock Elimination)
- 2.1 定義和原理
- 2.2 示例代碼
- 3. 鎖粗化(Lock Coarsening)
- 3.1 定義和原理
- 3.2 示例代碼
- 4. 幾種優化策略的聯系
- 三、鎖策略2
- 1. 可重入鎖 和 不可重入鎖
- 1.1 定義和原理
- 1.2 應用場景
- 1.3 示例代碼
- 1.4 圖文并茂
- 1. 初始狀態:線程 A 獲取鎖
- 2. 遞歸調用:線程 A 再次嘗試獲取鎖
- 3. 結束狀態:線程 A 釋放鎖
- 2. 公平鎖 和 非公平鎖
- 2.1 定義和原理
- 2.2 應用場景
- 2.3 示例代碼
- 2.4 圖文并茂
- 1. 公平鎖流程圖
- 閱讀順序:
- 2. 非公平鎖流程圖
- 閱讀順序:
- 3. 總結
- 四、CAS(Compare-And-Swap)問題
- 1. 通過偽代碼逐步引出 CAS 問題
- 1.1 簡單變量更新
- 1.2 引入無鎖更新的想法
- 1.3 引入比較-交換的思想
- 1.4 引出 CAS 操作的偽代碼
- 1.5 討論 CAS 操作的基本特性
- 1.6 總結 CAS 操作的應用場景
- 2. 通過 CAS 操作實現原子類
- 2.1 定義問題
- 2.2 引入無鎖更新的想法
- 2.3 引出 CAS 操作的偽代碼
- 2.4 實現真實的 CAS 操作
- 2.5 分析代碼的工作原理
- 2.6 圖文并茂說明
- 2.7 總結
- 3. 通過 CAS 操作實現自旋鎖
- 3.1 定義問題
- 3.2 引入無鎖更新的想法
- 3.3 引出 CAS 操作的偽代碼
- 3.4 實現真實的 CAS 操作
- 3.5 分析代碼的工作原理
- 3.6 圖文并茂說明
- 3.7 總結
- 4. CAS 中 ABA 問題
- 4.1 詳細介紹 CAS 中的 ABA 問題
- 4.2 輔助代碼示例
- 4.3 圖文并茂理解 ABA 問題
- 4.4 解決 ABA 問題的方法
- 4.5 總結
一、對比不同鎖策略之間的應用場景及其區別
在 Java 中,鎖策略的選擇對于多線程程序的性能和正確性至關重要。不同的鎖策略適用于不同的應用場景,理解它們的區別和聯系有助于編寫高效且線程安全的代碼。
1. 悲觀鎖 和 樂觀鎖
1.1 定義和原理
- 悲觀鎖(Pessimistic Locking) :假設沖突會發生,因此在每次訪問共享資源都加鎖,確保只有一個線程能夠訪問到資源。典型的實現包括
synchronized
和ReentrantLock
。 - 樂觀鎖(Optimistic Locking):假設沖突不會發生,只有在真正發生沖突的時候才采取措施。樂觀鎖通常通過版本號和時間戳來檢測沖突。典型實現包括 CAS(Compare And Swap)操作。
1.2 應用場景
- 悲觀鎖:適用于高并發環境的寫操作頻繁的場景。例如數據庫事務、文件系統等。
- 樂觀鎖:適用于讀操作遠多于寫操作場景。例如緩存,分布式系統中的數據一致性。
1.3 示例代碼
// 悲觀鎖示例:使用 synchronized
public class PessimisticLockExample {private int count = 0;public synchronized void increment() {count++;}public static void main(String[] args) throws InterruptedException {PessimisticLockExample example = new PessimisticLockExample();Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final count: " + example.count);}
}
// 樂觀鎖示例:使用 Atomic 類
import java.util.concurrent.atomic.AtomicInteger;public class OptimisticLockExample {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();}public static void main(String[] args) throws InterruptedException {OptimisticLockExample example = new OptimisticLockExample();Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final count: " + example.count.get());}
}
2. 重量級鎖 和 輕量級鎖
2.1 定義和原理
- 重量級鎖(Heavyweight Locking):基于操作系統級別的同步機制(如 mutex),當多個線程競爭同一把鎖時,線程就會掛起等待和上下文切換,開銷較大。
- 輕量級鎖(Lightweight Locking):通過 CAS 操作嘗試獲取鎖,避免重量級鎖的開銷。輕量級鎖適用于低競爭的場景。
2.2 應用場景
- 重量級鎖: 適用于高競爭的場景。例如數據庫連接池和文件系統等。
- 輕量級鎖: 適用于低競爭的場景。例如緩存和計數器等。
2.3 示例代碼
// 重量級鎖示例:使用 ReentrantLock
import java.util.concurrent.locks.ReentrantLock;public class HeavyweightLockExample {private final ReentrantLock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {HeavyweightLockExample example = new HeavyweightLockExample();Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final count: " + example.count);}
}
// 輕量級鎖示例:使用 Atomic 類
import java.util.concurrent.atomic.AtomicInteger;public class LightweightLockExample {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();}public static void main(String[] args) throws InterruptedException {LightweightLockExample example = new LightweightLockExample();Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final count: " + example.count.get());}
}
3. 掛起等待鎖 和 自旋鎖
3.1 定義和原理
- 掛起等待鎖(Blocking Lock):當線程無法獲取鎖時,就會掛起并進入等待狀態,直到鎖釋放。適用于長時間持有鎖的場景。
- 自旋鎖(Spin Lock):當線程無法獲取鎖時,會不斷地嘗試獲取鎖并不會進入等待狀態。適用于短時間持有鎖的場景。
3.2 應用場景
- 掛起等待鎖:適用于鎖持有時間長的場景。例如數據庫查詢,文件處理等。
- 自旋鎖:適用于鎖持有時間短的場景。例如緩存更新、計數器增加等。
3.3 示例代碼
// 掛起等待鎖示例:使用 ReentrantLock
import java.util.concurrent.locks.ReentrantLock;public class BlockingLockExample {private final ReentrantLock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {BlockingLockExample example = new BlockingLockExample();Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final count: " + example.count);}
}
// 自旋鎖示例:使用 SpinLock
public class SpinLock {private boolean isLocked = false;public synchronized void lock() {while (isLocked) {// 自旋等待}isLocked = true;}public synchronized void unlock() {isLocked = false;}
}public class SpinLockExample {private final SpinLock lock = new SpinLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {SpinLockExample example = new SpinLockExample();Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final count: " + example.count);}
}
4. 幾對鎖之間的聯系
- 悲觀鎖 VS 樂觀鎖:悲觀鎖適用于高競爭場景,樂觀鎖適用于低競爭場景。兩者都可以使用版本號和 CAS 等機制來結合使用。
- 重量級鎖 VS 輕量級鎖:重量級鎖適用于高競爭場景,輕量級鎖適用于低競爭場景。JVM 的子自適應鎖機制根據實際情況動態調整鎖的狀態。
- 掛起等待鎖 VS 自旋鎖:掛起等待鎖適用于鎖持有時間長的場景,自旋鎖適用于鎖持有時間短的場景。兩者可以根據鎖持有的時間來選擇使用。
5. 總結
- 悲觀鎖:適用于高競爭場景,線程安全但開銷較大。
- 樂觀鎖:適用于低競爭場景,通過版本號或 CAS 操作減少開銷。
- 重量級鎖:適用于高競爭場景,基于系統級別的同步機制。
- 輕量級鎖:適用于低競爭場景,通過 CAS 操作減少開銷。
- 掛起等待鎖:適用于長時間持有鎖的場景,減少 CPU 的 使用。
- 自旋鎖:適用于短時間持有鎖的場景,避免線程切換開銷。
二、synchronized 的優化策略
在 Java 中,synchronized
關鍵字是實現線程同步的重要工具。為了提高 synchronized
的性能,JVM 提供了多種優化策略,包括鎖優化,鎖消除,鎖粗化。這些優化策略可以顯著減少鎖的開銷,提高多線程程序的性能。
1. 鎖升級(Lazy Initialization)
1.1 定義和原理
鎖升級指 JVM 根據鎖競爭情況動態調整鎖的狀態。從偏向鎖到輕量級鎖再到重量級鎖的過程。這種機制類似于懶漢模式的思想,即在不需要時盡量做到輕量級的狀態,只有在競爭加劇的時候才轉化為重量級鎖。
- 無鎖狀態(Unlocked):對象沒有被任何線程鎖定。
- 偏向鎖狀態(Biased Locking):加入只有一個線程會訪問該對象,因此不需要每次都加鎖。偏向鎖通過在對象頭中加上線程 ID 來實現。
- 輕量級鎖狀態(Lightweight Locking):當有多個線程競爭同一把鎖時,偏向鎖失效,進入輕量級鎖狀態。輕量級鎖嘗試通過 CAS(
Compare And Swap
)操作來獲取鎖,避免重量級鎖的開銷。 - 重量級鎖狀態(Heavyweight Locking):當競爭進一步加劇的時候,CAS 操作失敗的次數過多,鎖會膨脹為重量級鎖,進入操作系統級別的同步機制。
1.2 鎖的狀態轉換圖
+-------------------+ +-------------------+ +-------------------+ +-------------------+
| | CAS | | CAS | | Fail | |
| Unlocked +------>+ Biased Lock +------>+ Lightweight Lock +------>+ Heavyweight Lock|
| | | | | | | |
+-------------------+ +-------------------+ +-------------------+ +-------------------+
1.3 應用場景
鎖升級適用于各種需要同步的場景,特別是在低競爭情況下能夠顯著提高性能。
- 如果一個鎖長時間沒有競爭,JVM 會選擇偏向鎖或輕量級鎖,減少加鎖的開銷。
- 如果鎖競爭變得激烈,JVM 會自動將鎖升級為重量級鎖,確保線程安全。
1.4 示例代碼
public class LockUpgradeExample {private int count = 0;public synchronized void increment() {count++;}public static void main(String[] args) throws InterruptedException {LockUpgradeExample example = new LockUpgradeExample();Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final count: " + example.count);}
}
2. 鎖消除(Lock Elimination)
2.1 定義和原理
鎖消除是指編譯器在運行代碼時自動識別代碼,識別出一些不必要的同步操作,將其優化掉。這可以通過逃逸分析(Escape Analysis
)來實現。即分析對象是否被其他線程訪問,如果沒有,就安全的消除同步操作。
2.2 示例代碼
public class LockEliminationExample {public static void main(String[] args) {long startTime = System.currentTimeMillis(); // 當前電腦系統的時間for (int i = 0; i < 10000000; i++) {performTask();}long endTime = System.currentTimeMillis();System.out.println("Time taken: " + (endTime - startTime) + " ms");}public static void performTask() {StringBuilder sb = new StringBuilder();sb.append("Hello");sb.append("World");// sb 對象僅在當前方法內使用,不會被其他線程訪問}
}
在這個例子中,StringBuilder
只在 performTask
方法中使用,不會被其他線程訪問到,JVM 可以通過逃逸分析識別出這個對象不需要同步分析,因此進行鎖消除。
未優化前:每次創建 StringBuilder
都需要同步操作。
優化后:通過逃逸分析識別出對象不會被其他線程訪問,消除同步操作。
3. 鎖粗化(Lock Coarsening)
3.1 定義和原理
鎖粗化是指將一系列連續的細粒度的加鎖操作合并成一次粗粒度的加鎖操作,從而減少鎖的開銷。鎖粗化通常應用在循環體內存在多次加鎖操作的情況。
3.2 示例代碼
public class LockCoarseningExample {private final Object lock = new Object();private int sum = 0;public void add(int value) {synchronized (lock) {sum += value;}}public static void main(String[] args) throws InterruptedException {LockCoarseningExample example = new LockCoarseningExample();Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.add(i);}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {example.add(i);}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final sum: " + example.sum);}
}
在這個例子中,add
方法在循環體內被多次調用,每次調用都需要進行同步操作。JVM 通過鎖粗化將多次加鎖操作合并成一次粗粒度的加鎖操作,從而減少鎖的開銷。
- 未優化前:每次調用
add
方法都需要進行加鎖操作,導致大量的上下文切換。 - 優化后:通過鎖粗化將多次加鎖操作合并成一次粗粒度的加鎖操作,減少上下文切換。
4. 幾種優化策略的聯系
- 鎖升級:通過動態調整鎖的狀態,從偏向鎖到輕量級鎖再到重量級鎖,減少鎖的開銷,適用于多種同步的場景,特別是低競爭情況下能夠顯著提升性能。
- 鎖消除:通過逃逸分析識別出不必要的同步操作,并將其優化掉。適用于那些在本地方法中創建的對象,且這些對象不會被其他線程訪問到的情況。
- 鎖粗化:通過多次加鎖操作合成一次粗粒度的加鎖操作,減少鎖的開銷。適用于那些在循環體內存在多次加鎖的操作場景。
三、鎖策略2
1. 可重入鎖 和 不可重入鎖
1.1 定義和原理
-
可重入鎖(Reentrant Lock):允許同一個線程多次獲取同一把鎖。每次獲取鎖時都會增加一個計數器,每次釋放鎖時都會減少計數器,只有當計數器歸零時才真正釋放鎖。
-
不可重入鎖(Non-reentrant Lock):不允許同一個線程多次獲取同一把鎖。如果一個線程已經持有了鎖,再次嘗試獲取鎖會導致死鎖。
1.2 應用場景
-
可重入鎖:適用于遞歸調用或需要在多個方法中使用同一把鎖的場景。
-
不可重入鎖:適用于簡單的同步控制場景,避免了不必要的復雜性和開銷。
1.3 示例代碼
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;public class ReentrantLockExample {private final Lock lock = new ReentrantLock();public void method1() {lock.lock();try {System.out.println("Method 1 acquired the lock.");method2(); // 遞歸調用} finally {lock.unlock();}}public void method2() {lock.lock();try {System.out.println("Method 2 acquired the lock.");} finally {lock.unlock();}}public static void main(String[] args) {ReentrantLockExample example = new ReentrantLockExample();example.method1();}
}
在這個例子中,method1
調用了 method2
,由于使用的是 ReentrantLock
,同一個線程可以多次獲取鎖而不會導致死鎖。
1.4 圖文并茂
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Thread A +-----> | Acquire Lock +-----> | Enter Critical |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Recursive Call +------>+ Acquire Again +------>+ Execute Code |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return Success <-------+ Release Lock <-------+ Finish Task |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
1. 初始狀態:線程 A 獲取鎖
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Thread A +-----> | Acquire Lock +-----> | Enter Critical |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
- Thread A:表示當前正在執行的線程。
- Acquire Lock:線程 A 嘗試獲取鎖。由于使用的是可重入鎖(如
ReentrantLock
),如果鎖沒有被其他線程持有,線程 A 可以成功獲取鎖。 - Enter Critical:一旦成功獲取鎖,線程 A 進入臨界區(Critical Section),可以安全地訪問共享資源。
2. 遞歸調用:線程 A 再次嘗試獲取鎖
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Recursive Call +------>+ Acquire Again +------>+ Execute Code |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
- Recursive Call:線程 A 在臨界區內調用了另一個方法,而該方法也需要獲取同一把鎖。由于使用的是可重入鎖,線程 A 可以再次成功獲取鎖。
- Acquire Again:線程 A 再次嘗試獲取鎖,由于是同一個線程,鎖的計數器增加,允許再次獲取鎖。
- Execute Code:線程 A 繼續執行代碼邏輯,訪問共享資源。
3. 結束狀態:線程 A 釋放鎖
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return Success <-------+ Release Lock <-------+ Finish Task |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
- Finish Task:線程 A 完成所有任務,準備退出臨界區。
- Release Lock:線程 A 釋放鎖。每次釋放鎖時,鎖的計數器減少一次。如果計數器歸零,則真正釋放鎖,允許其他線程獲取鎖。
- Return Success:線程 A 成功釋放鎖并返回結果。
2. 公平鎖 和 非公平鎖
2.1 定義和原理
-
公平鎖(Fair Lock):按照請求順序獲取鎖,即先請求的線程先獲得鎖。這種機制避免了“饑餓”現象,但可能導致較高的等待時間。
-
非公平鎖(Non-fair Lock):不保證請求順序,任何線程都有機會獲取鎖。這種機制通常具有更高的吞吐量,但可能導致某些線程長時間得不到鎖。
2.2 應用場景
-
公平鎖:適用于對響應時間要求較高、不能容忍“饑餓”現象的場景。
-
非公平鎖:適用于對吞吐量要求較高、可以容忍一定延遲的場景。
2.3 示例代碼
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;public class FairAndUnfairLockExample {private static final Lock fairLock = new ReentrantLock(true); // 公平鎖private static final Lock unfairLock = new ReentrantLock(false); // 非公平鎖public static void main(String[] args) throws InterruptedException {Runnable task = () -> {try {fairLock.lock();try {System.out.println(Thread.currentThread().getName() + " acquired fair lock.");Thread.sleep(1000); // 模擬長時間持有鎖} finally {fairLock.unlock();}} catch (InterruptedException e) {e.printStackTrace();}};for (int i = 0; i < 5; i++) {Thread thread = new Thread(task, "Thread-" + i);thread.start();}Thread.sleep(6000); // 等待所有線程完成task = () -> {try {unfairLock.lock();try {System.out.println(Thread.currentThread().getName() + " acquired unfair lock.");Thread.sleep(1000); // 模擬長時間持有鎖} finally {unfairLock.unlock();}} catch (InterruptedException e) {e.printStackTrace();}};for (int i = 0; i < 5; i++) {Thread thread = new Thread(task, "Thread-" + i);thread.start();}}
}
在這個例子中,我們創建了兩個鎖實例,一個是公平鎖,另一個是非公平鎖。通過觀察輸出結果,可以看到公平鎖按照請求順序獲取鎖,而非公平鎖則不一定。
2.4 圖文并茂
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Threads +-----> | Request Lock +-----> | Wait in Queue |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Fair Lock +------>+ Grant Lock +------>+ Execute Code |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return Success <-------+ Release Lock <-------+ Finish Task |
| | | | | |
+-------------------+ +-------------------+ +-------------------++-------------------+ +-------------------+ +-------------------+
| | | | | |
| Threads +-----> | Request Lock +-----> | Random Order |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Non-fair Lock +------>+ Grant Lock +------>+ Execute Code |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return Success <-------+ Release Lock <-------+ Finish Task |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
1. 公平鎖流程圖
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Threads +-----> | Request Lock +-----> | Wait in Queue |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Fair Lock +------>+ Grant Lock +------>+ Execute Code |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return Success <-------+ Release Lock <-------+ Finish Task |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
閱讀順序:
-
從左到右:
- 第一列:表示多個線程(Threads)。
- 第二列:表示線程請求鎖(Request Lock)。
- 第三列:表示線程進入等待隊列(Wait in Queue)。
- 第四列:表示線程執行代碼(Execute Code)。
-
從上到下:
- 線程首先嘗試請求鎖(Request Lock),如果鎖已經被其他線程持有,則進入等待隊列(Wait in Queue)。
- 當前持有鎖的線程釋放鎖后,公平鎖會按照請求順序依次授予鎖(Grant Lock),即先請求的線程先獲得鎖。
- 獲得鎖的線程開始執行臨界區代碼(Execute Code)。
- 執行完臨界區代碼后,線程釋放鎖(Release Lock),并返回成功(Return Success)。
2. 非公平鎖流程圖
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Threads +-----> | Request Lock +-----> | Random Order |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Non-fair Lock +------>+ Grant Lock +------>+ Execute Code |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return Success <-------+ Release Lock <-------+ Finish Task |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
閱讀順序:
-
從左到右:
- 第一列:表示多個線程(Threads)。
- 第二列:表示線程請求鎖(Request Lock)。
- 第三列:表示線程以隨機順序等待(Random Order)。
- 第四列:表示線程執行代碼(Execute Code)。
-
從上到下:
- 線程首先嘗試請求鎖(Request Lock),如果鎖已經被其他線程持有,則進入等待狀態(Random Order),但不保證按請求順序排隊。
- 當前持有鎖的線程釋放鎖后,非公平鎖可能會優先授予剛剛釋放鎖的線程或其他正在請求鎖的線程(Grant Lock),而不一定按照請求順序。
- 獲得鎖的線程開始執行臨界區代碼(Execute Code)。
- 執行完臨界區代碼后,線程釋放鎖(Release Lock),并返回成功(Return Success)。
3. 總結
-
可重入鎖 vs 不可重入鎖:
- 可重入鎖:允許同一個線程多次獲取同一把鎖,適用于遞歸調用或需要在多個方法中使用同一把鎖的場景。
- 不可重入鎖:不允許同一個線程多次獲取同一把鎖,適用于簡單的同步控制場景。
-
公平鎖 vs 非公平鎖:
- 公平鎖:按照請求順序獲取鎖,適用于對響應時間要求較高、不能容忍“饑餓”現象的場景。
- 非公平鎖:不保證請求順序,任何線程都有機會獲取鎖,適用于對吞吐量要求較高、可以容忍一定延遲的場景。
四、CAS(Compare-And-Swap)問題
1. 通過偽代碼逐步引出 CAS 問題
我們將從一個簡單的場景開始,逐步引出 CAS 操作的概念和其潛在的問題。
偽代碼:
boolean CAS(address, expectValue, swapValue) {if (&address == expectValue) {&address = swapValue;return true;}return false;
}
1.1 簡單變量更新
假設我們有一個共享的變量 value
,多個線程要對其進行更新操作。最直接的方法是對其進行加鎖操作保證線程安全。
public class SimpleUpdate {private int value = 0;public synchronized void update(int newValue) {value = newValue;}public static void main(String[] args) throws InterruptedException {SimpleUpdate updater = new SimpleUpdate();Thread t1 = new Thread(() -> updater.update(1));Thread t2 = new Thread(() -> updater.update(2));t1.start();t2.start();t1.join();t2.join();System.out.println("Final Value: " + updater.value);}
}
在這個例子中,我們使用 synchronized
關鍵字來確保只有一個線程對共享變量 value
進行更新操作,從而避免競爭條件。
1.2 引入無鎖更新的想法
雖然鎖機制可以確保線程安全,但它也有一定的開銷,特別在高并發環境下。因此,我們可以考慮一種無鎖的更新方法。下面是最初的嘗試:
public class NaiveUpdate {private int value = 0;public void update(int newValue) {// 直接更新值value = newValue;}public static void main(String[] args) throws InterruptedException {NaiveUpdate updater = new NaiveUpdate();Thread t1 = new Thread(() -> updater.update(1));Thread t2 = new Thread(() -> updater.update(2));t1.start();t2.start();t1.join();t2.join();System.out.println("Final Value: " + updater.value);}
}
這種方法存在明顯的競爭條件問題。兩個線程可能同時讀取和更新 value
,導致結果不可預測。
1.3 引入比較-交換的思想
為了確保線程安全,我們需要在更新之前檢查當前值(value
)是否與預期值(expectedValue
)一致。如果一致,就進行更新操作,如果不一致,就不做任何處理。這正是 CAS 操作的核心思想。
public class CompareAndSwapUpdate {private int value = 0;public boolean compareAndSwap(int expectedValue, int swapValue) {if (value == expectedValue) {value = swapValue;return true;}return false;}public static void main(String[] args) throws InterruptedException {CompareAndSwapUpdate updater = new CompareAndSwapUpdate();Thread t1 = new Thread(() -> {while (!updater.compareAndSwap(0, 1)) {// 自旋等待}});Thread t2 = new Thread(() -> {while (!updater.compareAndSwap(0, 2)) {// 自旋等待}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final Value: " + updater.value);}
}
在這個例子中,定義了一個 compareAndSwap
方法,首先檢查 value
是否和 expectedValue
相等。如果相等,就將 value
更新為 swapValue
,并返回 true
。如果不相等,不進行任何操作,并返回 false
。每個線程都在不斷地嘗試更新,直到成功為止。
1.4 引出 CAS 操作的偽代碼
回到最初的偽代碼:
boolean CAS(address, expectValue, swapValue) {if (&address == expectValue) {&address = swapValue;return true;}return false;
}
在這段偽代碼中:
address
:要修改的內存地址expectValue
:當前預期的值。swapValue
:要更新的新值。- 如果
&address
的值等于expectValue
,則將其更新為swapValue
并返回true
;否則返回false
。
1.5 討論 CAS 操作的基本特性
- 原子性:CAS 操作是一種原子操作,意味著它在硬件層面上是不可分割的,不會被其他線程打斷。
- 無鎖編程:CAS 操作不需要顯示的鎖,因此可以避免死鎖的開銷和上下文切換。
- 高效性:在低競爭的情況下,CAS 操作比鎖機制更加高效,因為它直接在硬件級別上進行原子操作。
1.6 總結 CAS 操作的應用場景
- 計數器:例如
AtomicInteger
類中的自增或自減。 - 緩存:用于更新緩存中的數據。
- 隊列:用于實現無鎖隊列等數據結構。
2. 通過 CAS 操作實現原子類
我們將通過一段偽代碼逐步引出如何使用 CAS 操作來實現簡單的原子類,并詳細分析其工作原理和應用場景。
偽代碼:
class AtomicInteger { private int value; public int getAndIncrement() { int oldValue = value; while ( CAS(value, oldValue, oldValue + 1) != true) { oldValue = value; } return oldValue; }
}
2.1 定義問題
假設我們需要實現一個線程安全的計數器,支持自增操作,最直接的辦法是使用鎖機制:
public class SimpleCounter {private int value = 0;public synchronized int getAndIncrement() {return value++;}public static void main(String[] args) throws InterruptedException {SimpleCounter counter = new SimpleCounter();Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.getAndIncrement();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final Value: " + counter.value);}
}
在這個例子中,使用 synchronized
關鍵字確保只有一個線程能夠訪問到 getAndIncrement
方法,從而避免競爭條件。然而,這種方式需要一定的開銷,特別是在高并發環境下。
2.2 引入無鎖更新的想法
為了提高性能,我們考慮使用一種無鎖的更新機制。CAS(Compare-And-Swap)操作可以實現這一點。下面是我們的初步嘗試:
public class NaiveAtomicInteger {private int value = 0;public boolean compareAndSwap(int expectedValue, int newValue) {if (value == expectedValue) {value = newValue;return true;}return false;}public static void main(String[] args) throws InterruptedException {NaiveAtomicInteger atomicInteger = new NaiveAtomicInteger();Thread t1 = new Thread(() -> {int oldValue = atomicInteger.value;while (!atomicInteger.compareAndSwap(oldValue, oldValue + 1)) {oldValue = atomicInteger.value;}});Thread t2 = new Thread(() -> {int oldValue = atomicInteger.value;while (!atomicInteger.compareAndSwap(oldValue, oldValue + 1)) {oldValue = atomicInteger.value;}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final Value: " + atomicInteger.value);}
}
在這個例子中,我們定義了一個 compareAndSwap
方法,它首先檢查 value
是否等于 expectedValue
,如果是,則將其更新為 newValue
并返回 true
;否則返回 false
。每個線程在一個循環中不斷嘗試更新,直到成功為止。
2.3 引出 CAS 操作的偽代碼
回到最初的偽代碼:
class AtomicInteger { private int value; public int getAndIncrement() { int oldValue = value; while ( CAS(value, oldValue, oldValue + 1) != true) { oldValue = value; } return oldValue; }
}
在這段偽代碼中:
value
:是要修改的共享變量。oldValue
:是當前讀取到的變量。CAS(value, oldValue, oldValue + 1)
:這是一個原子操作。第一步,先判斷oldValue
和value
是否相等。假如相等,就將其更新為oldValue + 1
,并返回true
;否則,返回false
。
2.4 實現真實的 CAS 操作
在 Java 中,可以通過 sun.misc.Unsafe 類或 java.util.concurrent.atomic 包中的類來實現。下面是使用 AtomicInteger 實現的完整示例:
import java.util.concurrent.atomic.AtomicInteger;public class AtomicCounter {private final AtomicInteger value = new AtomicInteger(0);public int getAndIncrement() {return value.getAndIncrement();}public static void main(String[] args) throws InterruptedException {AtomicCounter counter = new AtomicCounter();Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.getAndIncrement();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final Value: " + counter.value.get());}
}
在這個例子中,我們使用 AtomicInteger
類來實現一個線程安全的計數器。AtomicInteger
類內部實現了 CAS 操作,確保多個線程可以安全的進行自增操作。
2.5 分析代碼的工作原理
詳細分析一下 getAndIncrement
方法的工作原理:
- 讀取當前值:首先讀取
value
的值,并保存到oldValue
中。 - CAS 操作:嘗試將 value 更新為
oldValue + 1
。如果value
等于oldValue
,更新成功就返回true
;否則就返回false
。 - 重試機制:如果 CAS 操作失敗(value 值在此期間被其他線程修改),則重新讀取 value 并再次嘗試 CAS 操作,直到成功為止。
- 返回舊值:成功之后,返回最初的值
oldValue
,即使在此期間value
被多次修改。
2.6 圖文并茂說明
- 圖解 CAS 操作流程
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Current Value +-----> | Expected Value +-----> | New Value |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Compare +------>+ If Equal +------>+ Set New Value |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return True <-------+ Update Success <-------+ Return False |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
- 自旋等待機制
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Read Value +-----> | CAS Operation +-----> | Retry if Failed|
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return Old <-------+ Update Success <-------+ Read Again |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
2.7 總結
通過上述步驟,我們逐步引出了如何使用 CAS 操作來實現一個簡單的原子類,并詳細分析了其工作原理和應用場景。CAS 操作的核心思想是通過比較和交換實現無鎖編程,確保多線程環境下的安全性。盡管 CAS 操作有一些潛在的問題(如 ABA 問題和自旋開銷),但在低競爭情況下,它通常比鎖機制更高效。
3. 通過 CAS 操作實現自旋鎖
我們將通過一段偽代碼逐步引出自旋鎖的概念,并詳細分析其工作原理和應用場景。
偽代碼:
public class SpinLock {private Thread owner = null;public void lock() {// 通過 CAS 看當前鎖是否被某個線程持有。// 如果這個鎖已經被別的線程持有,那么就自旋等待。// 如果這個鎖沒有被別的線程持有,那么就把 owner 設置為當前嘗試加鎖的線程。while (!CAS(this.owner, null, Thread.currentThread())) {}}public void unlock() {this.owner = null;}
}
3.1 定義問題
假設我們需要一個簡單的互斥鎖(Mutex),確保只有一個線程在同一時間內訪問共享資源。最直接的方法是使用顯性的鎖機制。
public class SimpleLock {private boolean isLocked = false;public synchronized void lock() {while (isLocked) {try {wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}isLocked = true;}public synchronized void unlock() {isLocked = false;notifyAll();}public static void main(String[] args) throws InterruptedException {SimpleLock lock = new SimpleLock();Thread t1 = new Thread(() -> {lock.lock();try {System.out.println("Thread 1 acquired the lock.");Thread.sleep(1000); // 模擬長時間持有鎖} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();System.out.println("Thread 1 released the lock.");}});Thread t2 = new Thread(() -> {lock.lock();try {System.out.println("Thread 2 acquired the lock.");} finally {lock.unlock();System.out.println("Thread 2 released the lock.");}});t1.start();t2.start();t1.join();t2.join();}
}
在這個例子中,通過 synchronized
關鍵字來實現只有一個線程在一個時間段內訪問到共享資源,在鎖被釋放是喚醒等待的線程,但是這種方法存在一定的開銷,尤其在高并發的環境下。
3.2 引入無鎖更新的想法
為了提高性能,我們可以考慮使用一種無鎖的更新方法。CAS(Compare-And-Swap)操作可以幫助我們實現這一點。下面是我們的初步嘗試:
public class NaiveSpinLock {private Thread owner = null;public void lock() {Thread currentThread = Thread.currentThread();while (owner != null && owner != currentThread) {// 自旋等待}owner = currentThread;}public void unlock() {owner = null;}public static void main(String[] args) throws InterruptedException {NaiveSpinLock spinLock = new NaiveSpinLock();Thread t1 = new Thread(() -> {spinLock.lock();try {System.out.println(Thread.currentThread().getName() + " acquired the lock.");Thread.sleep(1000); // 模擬長時間持有鎖} catch (InterruptedException e) {e.printStackTrace();} finally {spinLock.unlock();System.out.println(Thread.currentThread().getName() + " released the lock.");}});Thread t2 = new Thread(() -> {spinLock.lock();try {System.out.println(Thread.currentThread().getName() + " acquired the lock.");} finally {spinLock.unlock();System.out.println(Thread.currentThread().getName() + " released the lock.");}});t1.start();Thread.sleep(100); // 確保 t1 先開始t2.start();t1.join();t2.join();}
}
在這個例子中,我們定義了一個 lock
方法,它首先檢查當前鎖是否被其他線程持有( owner != null
),并且判斷當前線程是否已經是獲取到鎖對象的線程(owner != currentThread
)。如果是,就將 owner
設置成當前線程;否則進入自旋等待狀態。每個線程在一個循環中不斷地嘗試獲取鎖,直到成功為止。
3.3 引出 CAS 操作的偽代碼
現在讓我們回到最初提供的偽代碼,并解釋它的含義:
owner
:表示當前持有鎖的線程。CAS(this.owner, null, Thread.currentThread())
:是一個原子操作,它首先檢查owner
是否為null
,檢查鎖是否已經被其他線程持有。如果是,則將其更新為當前線程并返回true
;否則返回false
。
3.4 實現真實的 CAS 操作
在 Java 中,CAS 操作可以通過 sun.misc.Unsafe
類或 java.util.concurrent.atomic
包中的類來實現。下面是一個使用 AtomicReference
實現的完整示例:
import java.util.concurrent.atomic.AtomicReference;public class SpinLock {private final AtomicReference<Thread> owner = new AtomicReference<>();public void lock() {Thread currentThread = Thread.currentThread();// 自旋等待,直到成功獲取鎖while (!owner.compareAndSet(null, currentThread)) {// 可以在這里添加一些優化,例如短暫休眠以減少 CPU 開銷// Thread.yield(); 或者 LockSupport.parkNanos(1L);}}public void unlock() {Thread currentThread = Thread.currentThread();if (owner.get() == currentThread) {owner.set(null);} else {throw new IllegalMonitorStateException("Calling thread does not hold the lock");}}public static void main(String[] args) throws InterruptedException {SpinLock spinLock = new SpinLock();Thread t1 = new Thread(() -> {spinLock.lock();try {System.out.println(Thread.currentThread().getName() + " acquired the lock.");Thread.sleep(1000); // 模擬長時間持有鎖} catch (InterruptedException e) {e.printStackTrace();} finally {spinLock.unlock();System.out.println(Thread.currentThread().getName() + " released the lock.");}});Thread t2 = new Thread(() -> {spinLock.lock();try {System.out.println(Thread.currentThread().getName() + " acquired the lock.");} finally {spinLock.unlock();System.out.println(Thread.currentThread().getName() + " released the lock.");}});t1.start();Thread.sleep(100); // 確保 t1 先開始t2.start();t1.join();t2.join();}
}
在這個例子中,使用 AtomicReference
類來實現自旋鎖。AtomicReference
內部實現了 CAS 操作,確保多個線程能夠安全地進行鎖操作。
3.5 分析代碼的工作原理
詳細分析一下 lock
和 unlock
方法的工作原理。
- 讀取當前值:首先讀取
owner
的值。 - CAS 操作:嘗試將
owner
更新為當前線程,如果當前的owner
為null
,則更新成功并返回true
;否則返回false
。 - 重試機制:如果 CAS 操作失敗(即
owner
在此期間被其他線程修改),則重新讀取當前的owner
并再次嘗試 CAS 操作,直到成功為止。 - 解除操作:當線程完成任務后,調用
unlock
方法將owner
設置為null
,允許其他線程獲取鎖。
3.6 圖文并茂說明
- 圖解自旋鎖流程
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Current Owner +-----> | Expected Owner +-----> | New Owner |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Compare +------>+ If Equal +------>+ Set New Owner |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return True <-------+ Update Success <-------+ Return False |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
- 自旋等待機制
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Read Owner +-----> | CAS Operation +-----> | Retry if Failed|
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return Old <-------+ Update Success <-------+ Read Again |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
3.7 總結
通過上述步驟,我們逐步引出了如何使用 CAS 操作來實現一個簡單的自旋鎖,并詳細分析了其工作原理和應用場景。自旋鎖的核心思想是通過不斷嘗試獲取鎖,適用于短時間持有鎖的場景,因為它避免了線程上下文切換的開銷。盡管自旋鎖有一些潛在的問題(如高 CPU 開銷),但在低競爭情況下,它通常比鎖機制更高效。
4. CAS 中 ABA 問題
4.1 詳細介紹 CAS 中的 ABA 問題
什么是 ABA 問題?
ABA 問題 (Atomicity, Consistency, and Availability)是指由于某個線程在讀取值后被其他線程修改兩次,最終又變回原來的值導致的問題。具體說明如下:
- 線程
A
讀取V
的值為A
。 - 線程
B
修改V
的值為B
,然后將V
的值改回A
。 - 當線程
A
檢查V
的值時,發現還是A
,認為變量沒有被修改過。
盡管變量 V
的值看起來沒有發生變化,但實際上是被修改過的,這可能會導致邏輯錯誤。
4.2 輔助代碼示例
下面是一個簡單的 Java 示例,展示了 ABA 問題的發生過程:
import java.util.concurrent.atomic.AtomicInteger;public class ABADemo {private static AtomicInteger value = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {// 線程 A 嘗試將 value 自增 1int expectedValue = value.get();while (!value.compareAndSet(expectedValue, expectedValue + 1)) {expectedValue = value.get();}System.out.println("Thread A: Value updated to " + value.get());});Thread t2 = new Thread(() -> {// 線程 B 先將 value 設為 1,再設回 0value.set(1);value.set(0);System.out.println("Thread B: Value set to 1 then back to 0");});t1.start();Thread.sleep(100); // 確保 t1 先開始t2.start();t1.join();t2.join();System.out.println("Final Value: " + value.get());}
}
在這個例子中,線程 A
通過 CAS 操作將 value
自增 1,而線程 B 則先將 value
設為 1,然后再設回 0。由于線程 A 在檢查 value
值的值時,發現它還是 0 ,就繼續進行自增操作。但實際上,value
值已經被修改過一次了,但還是識別不出來,這就是 ABA 問題。
4.3 圖文并茂理解 ABA 問題
- ABA 問題的流程圖
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Initial Value +-----> | Thread B Modify +-----> | Thread B Revert |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Read by Thread A+------>+ Check Value +------>+ Update Failed |
| | | | | |
+-------------------+ +-------------------+ +-------------------+| | |v v v
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Return Old <-------+ Update Success <-------+ Read Again |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
- ABA 問題的具體步驟
- 初始化:假設初始值為
value
。 - 線程 A:
- 讀取
value
值,保存到expectValue
中。 - 嘗試執行
CAS(value, 0, 1)
操作,如果成功,將value
更新成 1 ,如果失敗,則返回expectedValue
為 0 。
- 線程 B:先將
value
設置為 1 ;再將value
設回 0 。 - 線程 A 繼續嘗試:
- 讀取
value
值,保存到expectValue
中。 - 再次嘗試
CAS(value, 0, 1)
操作,因為當前value
值為 0,因此操作成功,將value
值更新為 1 。
- 結果:由于
value
值被修改過一次,但線程 A 還是無法察覺這一變化,繼續進行自增操作。
- 初始化:假設初始值為
4.4 解決 ABA 問題的方法
為了防止 ABA 問題的發生,可以使用帶有版本號或時間戳的 CAS 操作。Java 提供了 AtomicStampedReference
類來解決這個問題。
- 使用 AtomicStampedReference 解決 ABA 問題
import java.util.concurrent.atomic.AtomicStampedReference;public class ABASolution {private static AtomicStampedReference<Integer> value = new AtomicStampedReference<>(0, 0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {int[] stampHolder = new int[1];Integer currentValue = value.get(stampHolder);int currentStamp = stampHolder[0];boolean success = value.compareAndSet(currentValue, currentValue + 1, currentStamp, currentStamp + 1);System.out.println("Thread 1: " + success + ", New Value: " + value.getReference());});Thread t2 = new Thread(() -> {int[] stampHolder = new int[1];Integer currentValue = value.get(stampHolder);int currentStamp = stampHolder[0];value.compareAndSet(currentValue, currentValue + 1, currentStamp, currentStamp + 1);value.compareAndSet(currentValue + 1, currentValue, currentStamp + 1, currentStamp + 2);System.out.println("Thread 2: Value set to " + (currentValue + 1) + " then back to " + currentValue);});t1.start();Thread.sleep(100); // 確保 t1 先開始t2.start();t1.join();t2.join();System.out.println("Final Value: " + value.getReference());}
}
在這個例子中,我們使用 AtomicStampedReference
類來跟蹤變量的版本號(或時間戳)。每次修改變量,都會更新版本號。這樣,即使變量的值變回原來的狀態,版本號也會不一樣,就從而避免了 ABA 問題。
- AtomicStampedReference 的工作原理
AtomicStampedReference
使用一個整數作為版本號,與引用一起存儲。每次進行 CAS 操作時,不僅比較引用的值,還比較版本號。只有當引用的值和版本號都匹配時,才允許更新。
public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) {Pair<V> current = pair;returnexpectedReference == current.reference &&expectedStamp == current.stamp &&((newReference == current.reference &&newStamp == current.stamp) ||casPair(current, Pair.of(newReference, newStamp)));
}
4.5 總結
- ABA 問題:由于某個線程在讀取值后,被其他線程修改兩次,最終又變回原來的值導致的問題。
- CAS 操作中的 ABA 問題:CAS 操作只是讓當前的值和預期的值進行比較,而沒有考慮到中間是否會發生變化,這樣檢查不出 ABA 問題。
- 解決方案:使用帶版本號或時間戳的 CAS 操作(
AtomicStampedReference
),即使變量變回原來的狀態,版本號不同,從而避免 ABA 問題。