Compare-And-Swap (CAS) 是 Java 併發包(java.util.concurrent.atomic)實現無鎖線程安全的核心技術。但「無鎖 ≠ 高效」,在高併發爭用下 CAS 自旋可能嚴重拖垮效能。本文通過原理剖析、基準測試對比、JDK 鎖優化演進,揭示 CAS 適用邊界與最佳選擇策略。
一、CAS 原理與 AtomicInteger 實現
// AtomicInteger.getAndIncrement() 核心邏輯
public final int getAndIncrement() {
for (;;) { // 自旋循環
int current = get();
int next = current + 1;
if (compareAndSet(current, next)) // CAS 嘗試更新
return current;
// 失敗則重試
}
}
compareAndSet 底層調用 Unsafe.compareAndSwapInt → CPU LOCK CMPXCHG 指令
- 原子性由硬件保證,無需進入內核態、無線程切換開銷
二、適用場景對比:CAS vs synchronized
| 場景 | 推薦 | 理由 |
| 低併發、輕量操作 | CAS | 無上下文切換,自旋幾率低,延遲極低 |
| 高併發、長耗時操作 | synchronized | CAS 自旋消耗 CPU,synchronized 進入阻塞隊列更高效 |
| 計數器、狀態標記 | CAS (AtomicLong/AtomicReference) | 單變量原子操作,語義清晰 |
| 複雜臨界區、多變量協同 | synchronized / Lock | CAS 只能保證單變量原子性 |
三、基準測試:1000 線程累加 2000 萬次
public class CASBenchmark {
static final int THREADS = 1000;
static final int TARGET = 20_000_000;
AtomicInteger casCounter = new AtomicInteger(0);
int syncCounter = 0;
Object syncLock = new Object();
void casTest() throws InterruptedException {
Thread[] ts = new Thread[THREADS];
long start = System.currentTimeMillis();
for (int i = 0; i < THREADS; i++) {
ts[i] = new Thread(() -> {
while (casCounter.get() < TARGET) casCounter.getAndIncrement();
});
ts[i].start();
}
for (Thread t : ts) t.join();
System.out.println("CAS 耗時: " + (System.currentTimeMillis() - start) + "ms");
}
void syncTest() throws InterruptedException {
Thread[] ts = new Thread[THREADS];
long start = System.currentTimeMillis();
for (int i = 0; i < THREADS; i++) {
ts[i] = new Thread(() -> {
while (true) {
synchronized (syncLock) {
if (syncCounter >= TARGET) break;
syncCounter++;
}
}
});
ts[i].start();
}
for (Thread t : ts) t.join();
System.out.println("synchronized 耗時: " + (System.currentTimeMillis() - start) + "ms");
}
}
測試結果(雙核 CPU,JDK 1.7)
- 純計數:CAS ~4500ms,synchronized ~1200ms → CAS 完敗(自旋過度消耗 CPU)
- 加入 40ms 文件讀取模擬業務:CAS ~42000ms,synchronized ~42000ms → 持平(業務耗時掩蓋同步開銷)
四、為什麼 synchronized 反超 CAS?
JDK 1.6+ 引入重磅優化:
- 偏向鎖:無競爭時無 CAS 開銷,直接標記線程 ID
- 輕量級鎖:輕度競爭用 CAS 自旋,避免進入內核
- 自適應自旋:根據歷史自旋成功率動態調整自旋次數
- 鎖消除/鎖粗化:JIT 編譯器優化冗餘同步
- 阻塞隊列:重度競爭直接掛起線程,不佔用 CPU
綜合效果:synchronized 在中高併發下綜合效能優於手寫 CAS 循環。
五、CAS 典型陷阱與避坑指南
- ABA 問題:值 A→B→A,CAS 認為未變。解決:
AtomicStampedReference 帶版本號
- 自旋過度:高併發長循環燒 CPU。解決:改用 synchronized 或 LongAdder(分段計數)
- 只能單變量:多變量不變性需用鎖或 STM
- 內存屏障語義:CAS 具有 volatile 讀寫語義,但非萬能
六、LongAdder:高併發計數器的正解
LongAdder adder = new LongAdder();
adder.increment(); // 內部多段槽位分散熱點,最終 sum() 聚合
// 效能遠超 AtomicLong,適合統計計數、監控指標
七、總結
CAS 是併發編程的基石,但不是銀彈。低併發輕操作用 CAS(Atomic*),中高併發或複雜邏輯用 synchronized/Lock,超高併發計數用 LongAdder。理解 JDK 鎖優化演進,信任 synchronized 的現代表現,才是成熟併發工程師的標誌。
- 上一篇文章: Struts2 JSON 序列化懶加載異常解決方案:黑名單過濾與 Jar 包衝突排查
- 下一篇文章: 堆數據結構與堆排序:Java 代碼演示上浮、下沉、建堆全過程
Copyright ©
程式碼之家 All Rights Reserved