目錄
1.前言
2.正文
2.1CAS概念
2.2CAS兩種用途
2.2.1實現原子類
2.2.2實現自旋鎖
2.3缺陷:ABA問題
2.4JUC組件
2.4.1Callable接口
2.4.2ReentrantLock(與synchronized對比)
2.4.3Semaphore信號量
2.4.4CountDownLatch
3.小結
1.前言
哈嘍大家好吖,不知不覺多線程這一塊大骨頭終于快要啃完了,今天給大家分享的是CAS以及JUC相關組件,那么廢話不多說讓我們開始吧。
2.正文
2.1CAS概念
核心思想:無所并發控制
CAS(Compare And Swap)是一種基于樂觀鎖的無鎖并發控制技術。其核心邏輯可以概括為:“我認為當前值應該是A,如果是,則更新為B;否則放棄或重試”。整個過程由硬件保證原子性,無需傳統鎖機制。
通俗來說
假設你和同事協同編輯一份共享文檔,每次保存時系統會檢查:
當前內容是否和你打開時的版本一致(預期值比對)。
如果一致,允許保存;否則提示“內容已變更,請重新編輯”。
這個過程就是CAS的核心思想——樂觀鎖:先操作,沖突時重試,而非直接加鎖阻塞。
CAS操作的偽代碼可以拆解為以下步驟,幫助理解其原子性本質:
// 偽代碼:CAS操作的邏輯分解
public boolean compareAndSwap(MemoryAddress addr, int expectedValue, int newValue) {// 1. 讀取內存當前值int currentValue = *addr; // 2. 比較當前值與預期值if (currentValue != expectedValue) {return false; // 值已被其他線程修改,操作失敗}// 3. 若值未變,執行原子性更新*addr = newValue;return true;
}
2.2CAS兩種用途
2.2.1實現原子類
針對原子類,++--這樣的操作是原子的,基于CAS實現,不涉及到加鎖。
傳統實現:
private int count = 0;
public synchronized void increment() { count++;
}
進階實現: (使用Java提供的原子類)
AtomicInteger count = new AtomicInteger(0);
public void increment() { int oldValue, newValue; do { oldValue = count.get(); newValue = oldValue + 1; } while (!count.compareAndSet(oldValue, newValue)); // CAS自旋
}
2.2.2實現自旋鎖
先回顧一個上篇文章的概念:自旋鎖是線程通過循環(自旋)不斷嘗試獲取鎖,而非立即阻塞。適用于鎖持有時間極短的場景。
代碼實現:
public class CASSpinLock { private AtomicBoolean locked = new AtomicBoolean(false); // 獲取鎖 public void lock() { while (!locked.compareAndSet(false, true)) { // 自旋:直到成功將locked從false改為true } } // 釋放鎖 public void unlock() { locked.set(false); }
}
線程競爭不激烈時(如短任務),自旋鎖比系統鎖(如
synchronized
)更高效。缺點:長時間自旋會浪費CPU資源(需根據場景權衡)。
2.3缺陷:ABA問題
ABA問題場景
-
線程1讀取變量值為
A
。 -
線程2將值改為
B
,隨后又改回A
。 -
線程1執行CAS操作,發現當前值仍是
A
,誤認為未被修改過,操作成功。
通俗理解:
你看到自己的水杯是滿的(A),去接水時離開了一會兒。
期間別人喝光水(A→B)又倒滿(B→A)。
你回來后以為水沒被喝過,直接喝下(可能喝到別人的水!)。
這里在實際場景中就是非常嚴重的線程安全的問題了。
解決方案:?
?
1. 版本號標記(AtomicStampedReference)
為值附加一個版本號(類似“修改次數”),CAS時同時校驗值和版本號。AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); // 線程1讀取值和版本號 int stamp = ref.getStamp(); String oldValue = ref.getReference(); // 線程2修改值并更新版本號 ref.compareAndSet("A", "B", stamp, stamp + 1); ref.compareAndSet("B", "A", stamp + 1, stamp + 2); // 線程1嘗試修改:雖然值還是A,但版本號已變,操作失敗! boolean success = ref.compareAndSet(oldValue, "C", stamp, stamp + 1);
2. 狀態標記(AtomicMarkableReference)
用布爾值標記是否被修改過(簡化版版本號)。
2.4JUC組件
2.4.1Callable接口
官方解析:Callable (Java SE 17 & JDK 17)
Callable
?是 Java 并發包(JUC)中定義的接口,類似于?Runnable
,但允許線程執行任務后返回結果,并可以拋出異常。與?
Runnable
?的區別:
Runnable
?的?run()
?沒有返回值,Callable
?的?call()
?可以返回泛型結果。
call()
?可以拋出受檢異常,run()
?不能。
具體案例(異步運算1加到100):
Callable<Integer> task = () -> {int sum = 0;for (int i = 1; i <= 100; i++) sum += i;return sum;
};
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();// 主線程獲取結果
System.out.println("計算結果:" + futureTask.get()); // 輸出 5050
通過?
FutureTask
?包裝?Callable
?任務,啟動線程執行后,主線程通過?futureTask.get()
?等待結果返回,類似“異步任務+回調”模式。?
2.4.2ReentrantLock(與synchronized對比)
官方解析:ReentrantLock (Java SE 17 & JDK 17)
ReentrantLock
?是 JUC 提供的顯式鎖,支持可重入性、可中斷鎖、公平鎖等特性。
特性 | synchronized | ReentrantLock |
---|---|---|
鎖獲取方式 | 隱式(JVM 管理) | 顯式(代碼手動加鎖/解鎖) |
可中斷 | 不支持 | 支持?lockInterruptibly() |
公平鎖 | 不支持 | 支持(構造函數指定) |
條件變量(Condition) | 無 | 支持(newCondition() ) |
?案例:
class BankAccount {private final ReentrantLock lock = new ReentrantLock();private int balance = 100;void transfer(BankAccount target, int amount) {lock.lock();try {if (this.balance >= amount) {this.balance -= amount;target.balance += amount;}} finally {lock.unlock(); // 必須手動釋放鎖}}
}
synchronized
?的等價實現是在方法簽名加?synchronized
?關鍵字,但?ReentrantLock
?更靈活:
可設置超時時間(
tryLock(1, TimeUnit.SECONDS)
)。公平鎖減少線程饑餓問題。
2.4.3Semaphore信號量
官方解析:Semaphore (Java SE 17 & JDK 17)
Semaphore
?用于控制同時訪問某個資源的線程數量,類似“許可證發放”。
核心方法:
acquire()
:獲取許可證(若無可用則阻塞)。
release()
:釋放許可證。
?案例:(模擬停車場)
Semaphore semaphore = new Semaphore(3); // 3 個許可證Runnable parkAction = () -> {try {semaphore.acquire(); // 獲取車位System.out.println(Thread.currentThread().getName() + " 停入車位");Thread.sleep(2000); // 停車 2 秒} catch (InterruptedException e) {e.printStackTrace();} finally {semaphore.release(); // 釋放車位System.out.println(Thread.currentThread().getName() + " 離開車位");}
};// 啟動 5 輛車嘗試停車
for (int i = 0; i < 5; i++) {new Thread(parkAction).start();
}
2.4.4CountDownLatch
官方解析:CountDownLatch (Java SE 17 & JDK 17)
CountDownLatch
?是一個同步工具,允許一個或多個線程等待其他線程完成操作。
核心方法:
countDown()
:計數器減 1。
await()
:阻塞直到計數器歸零。?
public static void main(String[] args) {CountDownLatch latch = new CountDownLatch(3); // 需要等待 3 個任務// 資源加載任務Runnable loadTask = () -> {try {Thread.sleep((long) (Math.random() * 2000));System.out.println(Thread.currentThread().getName() + " 加載完成");latch.countDown();} catch (InterruptedException e) {e.printStackTrace();}};// 啟動 3 個資源加載線程new Thread(loadTask, "地圖").start();new Thread(loadTask, "音效").start();new Thread(loadTask, "UI").start();// 主線程等待所有資源加載完成try {latch.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("所有資源加載完成,開始游戲!");}
3.小結
今天的分享到這里就結束了,喜歡的小伙伴點點贊點點關注,你的支持就是對我最大的鼓勵,大家加油!
?