大家好,我是你們的老伙計秀才!今天帶來的是[深入淺出Java多線程]系列的第十篇內容:CAS。大家覺得有用請點贊,喜歡請關註!秀才在此謝過大家了!!! ...
引言
大家好,我是你們的老伙計秀才!今天帶來的是[深入淺出Java多線程]系列的第十篇內容:CAS。大家覺得有用請點贊,喜歡請關註!秀才在此謝過大家了!!!
在多線程編程中,對共用資源的安全訪問和同步控制是至關重要的。傳統的鎖機制,如synchronized關鍵字和ReentrantLock等,能夠有效防止多個線程同時修改同一數據導致的競態條件(race condition),但同時也帶來了一定的性能開銷。尤其是在高併發場景下,頻繁的加鎖解鎖操作可能導致線程上下文切換加劇、系統響應延遲等問題。
為了應對這一挑戰,Java從JDK 1.5版本開始引入了基於CAS(Compare And Swap)機制的原子類庫,這些原子類不僅提供了一種無鎖化的併發控制策略,還能夠在不阻塞其他線程的情況下實現高效的記憶體同步。CAS作為樂觀鎖的一種實現方式,其核心思想是在更新變數時僅當該變數的當前值與預期值相等時才會執行更新操作,否則就放棄更新並允許線程繼續嘗試或採取其他策略。
例如,在一個簡單的場景中,假設有一個被多個線程共用的整型變數i
,若我們想要通過CAS將其從初始值5原子性地遞增到6,可以利用AtomicInteger類中的compareAndSet方法:
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private static AtomicInteger sharedValue = new AtomicInteger(5);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
int oldValue = sharedValue.get();
if (sharedValue.compareAndSet(oldValue, oldValue + 1)) {
System.out.println("Thread " + Thread.currentThread().getName() + " updated the value to " + (oldValue + 1));
break;
}
}
});
t1.start();
// 確保t1有機會更新值
t1.join();
// 輸出結果應為:Thread Thread-0 updated the value to 6
}
}
在這個示例中,如果sharedValue
的當前值確實是5,那麼線程t1
將成功地將它更改為6,並退出迴圈;如果有其他線程在此期間改變了sharedValue
的值,則t1
會不斷重試直至成功。由於CAS操作直接由CPU指令級別保證其原子性,因此不會出現因併發寫入導致的數據混亂。
通過深入探討Java多線程中的CAS技術,我們將揭示其背後的具體實現原理——Unsafe類及其native方法,剖析AtomicInteger等原子類如何藉助CAS機制實現在無鎖環境下的高效併發操作,併進一步討論在實際應用中可能出現的問題,如ABA問題、迴圈自旋消耗過大以及只能針對單個變數進行原子操作的局限性及其相應的解決方案。
在多線程編程領域中,鎖機制是實現數據同步和避免併發問題的關鍵手段。其中,樂觀鎖與悲觀鎖作為兩種不同的併發控制策略,在處理共用資源時採用了截然不同的假設和操作方式。
悲觀鎖&樂觀鎖
悲觀鎖
悲觀鎖,顧名思義,採取保守的策略對待併發訪問。它假定每次對共用資源進行操作時都可能發生衝突,因此在執行任何更新前都會預先鎖定資源。例如,在Java中使用synchronized
關鍵字或ReentrantLock
等工具實現悲觀鎖時,一個線程在獲取鎖後才能進入臨界區執行代碼,其他線程則必須等待鎖釋放後才能獲得執行機會。以下是一個簡單的悲觀鎖示例:
public class PessimisticLockExample {
private final Lock lock = new ReentrantLock();
public void decrementCounter() {
lock.lock(); // 獲取悲觀鎖
try {
// 臨界區代碼
int count = this.count;
if (count > 0) {
this.count--;
}
} finally {
lock.unlock(); // 釋放悲觀鎖
}
}
// 共用資源變數
private int count = 10;
}
在這個例子中,當一個線程試圖修改計數器時,會先鎖定整個方法,確保同一時間只有一個線程能夠執行減一操作。這種機制雖然保證了數據一致性,但可能造成線程間的頻繁阻塞和上下文切換,尤其在高併發環境下性能損耗明顯。
樂觀鎖
相對而言,樂觀鎖則是基於積極樂觀的假設:認為大部分情況下多個線程同時訪問同一資源並不會發生衝突。因此,樂觀鎖允許線程無須獲取鎖就可以執行業務邏輯,僅在更新數據時採用CAS(Compare And Swap)原子操作檢查並更新數據。如果發現數據已被其它線程改變,則放棄本次更新,通常會重新讀取數據並再次嘗試。
以Java中的AtomicInteger為例,它利用CAS機制實現了樂觀鎖的特性:
public class OptimisticLockExample {
private final AtomicInteger counter = new AtomicInteger(10);
public void incrementCounter() {
while (true) { // 自旋
int currentValue = counter.get();
int newValue = currentValue + 1;
if (counter.compareAndSet(currentValue, newValue)) { // 使用CAS原子操作
break; // 更新成功,退出迴圈
}
}
}
}
// AtomicInteger 的 compareAndSet 方法源碼簡化示意
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
上述代碼展示瞭如何在一個迴圈內連續嘗試原子地增加計數器值。只有噹噹前值等於預期值時,CAS操作才會成功,否則線程將不斷重試直至成功更新。由於樂觀鎖在沒有衝突的情況下不涉及線程掛起,故適用於“讀多寫少”的場景,能有效降低加鎖開銷,提高系統吞吐量。然而,若併發更新頻率較高,可能會導致大量的CAS失敗和重試,從而帶來額外的CPU消耗。
CAS原理
在併發編程中,CAS(Compare and Swap,比較並交換)是一種無鎖演算法,它在不阻塞其他線程的情況下實現原子性的變數更新操作。在Java中,CAS的實現基於Unsafe類提供的native方法,這些方法直接與底層硬體交互,利用CPU級別的原子指令來保證數據更新的安全性。
CAS流程
在CAS操作中涉及三個關鍵值:V(要更新的變數),E(預期值),N(新值)。當需要對一個共用變數進行修改時,線程首先檢查該變數當前值是否等於預期值E。如果相等,則將變數值更新為新值N;如果不等,則說明已經有其他線程更新了該變數,此時當前線程放棄更新操作,保持原值不變。
以AtomicInteger為例,我們可以通過以下代碼片段理解CAS的工作過程:
import java.util.concurrent.atomic.AtomicInteger;
public class CASTest {
private AtomicInteger counter = new AtomicInteger(5);
public void increment() {
int expectedValue = counter.get();
while (!counter.compareAndSet(expectedValue, expectedValue + 1)) {
// 當前線程獲取到的值已經被其他線程改變,重新獲取最新值
expectedValue = counter.get();
}
}
}
在這個例子中,compareAndSet
方法會不斷嘗試將計數器從舊值遞增1,直到成功為止。當多個線程同時嘗試增加計數器時,只有一個線程能夠通過CAS成功更新,其餘線程將繼續迴圈直至其看到的預期值和實際值匹配後再嘗試更新。
原子性和操作系統
CAS的核心優勢在於其原子性——即整個比較和交換的操作作為一個不可分割的整體執行。在現代多核CPU架構下,諸如cmpxchg指令這樣的原子指令能夠確保在沒有外部干預的情況下完成這一系列步驟。在Linux X86系統中,cmpxchgl指令配合lock首碼可以確保在同一時刻僅有一個處理器能成功更新記憶體位置,從而避免了併發問題。
ABA問題
儘管CAS機制在大多數情況下表現優異,但存在一種特殊情況——ABA問題。假設一個變數初始值為A,被更改為B後又改回A,這種情況下使用單純的CAS檢查將會誤判為未發生過變化。為了應對ABA問題,JDK提供了一個名為AtomicStampedReference的類,它在每個對象引用上附加了一個版本號或時間戳,使得每次更改不僅檢查引用本身,還檢查版本號,只有兩者都匹配時才會進行替換。
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABATest {
private AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(1, 0);
public void update(int newValue, int newStamp) {
while (true) {
int currentStamp = ref.getStamp();
if (ref.compareAndSet(1, newValue, currentStamp, newStamp)) {
break; // 更新成功
} else {
// 失敗則重試,獲取最新的stamp
}
}
}
}
在上述代碼中,compareAndSet
方法不僅要比較引用對象的值,還要比較並更新相關聯的版本信息,因此有效防止了ABA問題的發生。
綜上所述,CAS作為一種高效的無鎖同步機制,在Java多線程編程中扮演著重要角色,通過直接調用CPU指令實現了併發環境下的原子操作,但也需要註意潛在的ABA問題以及長時間自旋帶來的性能開銷等問題,並選擇合適的解決方案。
Unsafe類
在Java中,為了能夠直接與底層硬體進行交互並執行原子操作,如CAS,Java使用了一個名為sun.misc.Unsafe
的類。由於該類提供了一些不受JVM訪問控制約束的方法,並允許開發者直接操作記憶體和執行非安全但高效的原語操作,因此被稱為“Unsafe”。儘管這個類不在公共API中,但在併發包java.util.concurrent.atomic
中的原子類,如AtomicInteger等,都依賴於Unsafe類提供的CAS操作來保證線程間的原子性和可見性。
Unsafe類與CAS方法 Unsafe類包含了一系列native方法,這些方法用於執行原子性的CAS操作,例如:
public native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
public native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
public native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
這些方法分別用於比較並交換對象引用、整型值以及長整型值。參數含義如下:
o
:一個對象實例,CAS操作將作用在其內部的一個欄位上。offset
:指定欄位相對於對象起始地址的偏移量,由objectFieldOffset()
方法計算得出。expected
:期望的舊值,只有當欄位當前值等於此預期值時,才會進行更新。x
:新值,如果條件滿足,則用新值替換舊值。
以AtomicInteger為例,其getAndAddInt方法就利用了Unsafe類的compareAndSwapInt方法實現原子遞增:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 獲取當前值
} while (!weakCompareAndSetInt(o, offset, v, v + delta)); // 使用CAS嘗試更新
return v; // 返回更新前的值
}
這裡首先獲取到共用變數的當前值v,然後在一個迴圈中不斷嘗試通過CAS指令將變數從v更新為v+delta,直到成功為止。
CPU級別的原子操作 值得註意的是,CAS操作在Java中的實現實際上調用了操作系統和CPU提供的原子指令。在Linux X86系統下,是通過cmpxchgl這樣的CPU指令實現的,而在多處理器環境中,為了確保跨多個CPU核心的原子性,還需要配合lock首碼指令鎖定匯流排或緩存行,防止其他處理器同時修改同一數據。
弱版本CAS與強版本CAS的區別
從JDK 9開始,Unsafe類提供了兩個看似相似但實際上可能有不同實現策略的方法:compareAndSetInt
和weakCompareAndSetInt
。雖然在早期版本中它們的行為一致,但在某些情況下,weakCompareAndSet
系列方法可能只保留了volatile變數本身的特性,而放棄了happens-before規則帶來的記憶體語義保障。這意味著weakCompareAndSet
無法確保除了目標volatile變數以外的其他變數的操作順序和可見性,從而有可能帶來更高的性能,但也可能需要開發人員更小心地處理併發邏輯。
總之,Java通過Unsafe類實現了對CAS原子操作的支持,使得程式員可以在高級語言層面上利用底層硬體的原子指令,構建出高效且無鎖化的併發程式。然而,這也要求開發者具備對併發編程機制深刻的理解,以便正確解決潛在的問題,比如ABA問題,以及合理應對CAS自旋可能導致的性能開銷。
AtomicInteger源碼簡析
Java併發包中的java.util.concurrent.atomic.AtomicInteger
類是一個基於CAS實現的線程安全整數容器,它提供了一系列原子操作方法,如get、set、incrementAndGet等。以getAndAdd(int delta)
方法為例,該方法用於獲取當前值並原子性地將值增加指定的delta。
Java 17下的Atomic類:
首先,我們觀察到getAndAdd(int delta)
方法調用了Unsafe
類的getAndAddInt()
方法:
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
這裡的U
是Unsafe
類的一個實例,其內部欄位VALUE存儲了AtomicInteger
類中value
變數相對於對象起始地址的偏移量。objectFieldOffset()
方法用於計算這個偏移量:
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
然後,深入到Unsafe
類的getAndAddInt()
方法實現:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 獲取volatile類型的舊值
} while (!weakCompareAndSetInt(o, offset, v, v + delta)); // 使用CAS更新新值
return v; // 返回更新前的值
}
這段代碼展示了典型的CAS迴圈模式。首先通過getIntVolatile()
讀取記憶體中AtomicInteger
實例的volatile變數value
的當前值,並保存在局部變數v
中。接下來進入一個do-while迴圈,在迴圈體內嘗試使用weakCompareAndSetInt()
執行CAS操作。只有當value
的當前值等於我們剛讀取到的v
時,才會將value
設置為v+delta
。如果此時