原子操作意為“不可被中斷的一個或一系列操作”。
處理器實作原子操作
1、通過總線鎖保證原子性
所謂總線鎖就是使用處理器提供的一個
LOCK#
信号,當一個處理器在總線上輸出此信号時,其他處理器的請求将被阻塞住,那麼該處理器可以獨占共享記憶體。
2、通過緩存鎖定來保證原子性
所謂“緩存鎖定”是指記憶體區域如果被緩存在處理器的緩存行中,并且在Lock操作期間被鎖定,那麼當它執行鎖操作回寫到記憶體時,處理器不在總線上聲言
LOCK#
信号,而是修改内部的記憶體位址,并允許它的緩存一緻性機制來保證操作的原子性,因為緩存一緻性機制會阻止同時修改由兩個以上處理器緩存的記憶體區域資料。
但是兩種情況下處理器不會使用緩存鎖定
- 當操作資料不能被緩存在處理器内部,或操作的資料跨多個緩存行時,則處理器會調用總線鎖定。
- 處理器不支援,例如Intel 486和Pentium處理器
Java中實作原子操作
在Java中通過鎖和循環CAS的方式來實作原子操作
CAS簡介
CAS的全稱Compare And Swap,直譯就是比較和交換,一種無鎖原子算法。過程是這樣:它包含3個參數CAS(V, E, N),V表示要更新變量的值,E表示預期值,N表示新值。僅當V=E時,才會将V的值設為N,如果V值和E值不同,則目前線程什麼都不做。簡單來說,CAS 需要你額外給出一個期望值,也就是你認為這個變量現在應該是什麼樣子的。如果變量不是你想象的那樣,則說明它已經被别人修改過了,你就需要重新讀取(自旋),再次嘗試修改就好了。
與鎖相比,使用CAS會使程式看起來更加複雜一些,但由于其非阻塞的,它對死鎖問題天生免疫,并且,線程間的互相影響也非常小。更為重要的是,使用無鎖的方式完全沒有鎖競争帶來的系統開銷,也沒有線程間頻繁排程帶來的開銷,是以,它要比基于鎖的方式擁有更優越的性能
CAS底層原理
CAS歸功于硬體指令集的發展,硬體保證一個從語義上看起來需要多次操作的行為隻通過一條處理器指令就能完成。這類指令常用的有:
- 測試并設定(Test and Set)
- 擷取并增加(Fetch and Increment)
- 交換(Swap)
- 比較并交換(Compare and Swap)
- 加載連結/條件存儲(Load Linked/Store Conditional)
java.util.concurrent.atomic
包裡的atomic類都是通過CAS來實作的,下面以AtomicInteger為例,來看下CAS的具體實作。

比如說AtomicInteger中的
incrementAndGet()
方法,内部調用
compareAndSwapInt()
方法,而
compareAndSwapInt()
是本地native方法。
更底層是利用處理器提供的CMPXCHG指令實作的,是以說CAS需要底層硬體的支援。
代碼示例:
public class Counter {
private static AtomicInteger atomicI = new AtomicInteger(0);
private static int i = 0;
public static void main(String[] args) {
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(() -> {
Counter.count();
Counter.safeCount();
});
threads[i].start();
}
System.out.println(Counter.i);
System.out.println(Counter.atomicI.get());
}
private static void safeCount() {
while (true) {
int i = atomicI.get();
boolean suc = atomicI.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
// unsafe
private static void count() {
i++;
}
}
CAS實作原子操作的三大問題
循環時間長開銷大
自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷
隻能保證一個共享變量的原子操作
循環CAS隻能保證一個共享變量的原子操作,當對多個共享變量操作時,就需要使用鎖了,或者把多個共享變量合并成一個共享變量來操作。例如:AtomicReference類保證引用對象之間的原子性,可以把多個變量放在一個對象裡來進行CAS操作。
ABA問題
如果一個值原來是A,變成了B,又變成了A。那麼根據CAS的算法過程,在對預期值進行檢查時會發現沒有改變,但實際上卻變化了,這就是ABA問題。ABA問題的解決思是在變量前追加版本号,比如:A1-B2-A3。從JDK1.5開始,
java.util.concurrent.atomic
包中提供了AtomicStampedReference來解決ABA的問題,這個類的
compareAndSet()
方法的作用是首先檢查目前引用是否等于預期引用,并且檢查目前标志是否等于預期标志,如果全部相等,則以原子方式将該引用和該标志的值設定為給定的更新值。
下面我們來看個執行個體:
public class ABADemo {
private static AtomicStampedReference asr = new AtomicStampedReference(100, 1);
public static void main(String[] args) throws InterruptedException {
// 模拟ABA問題
Thread th1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(asr.compareAndSet(100, 110, asr.getStamp(), asr.getStamp() + 1));
System.out.println(asr.compareAndSet(110, 100, asr.getStamp(), asr.getStamp() + 1));
});
th1.start();
// 先取出stamp,然後等ABA操作後去更新A的值
Thread th2 = new Thread(() -> {
int stamp = asr.getStamp();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(asr.compareAndSet(100, 120, stamp, stamp + 1));
});
th2.start();
}
}
操作結果如下:
可以發現ABA操作後,如果再使用原先的stamp去做更新,會導緻更新失敗。
參考資料
Java并發程式設計的藝術 方騰飛 魏鵬 程曉明 著
Java并發程式設計實戰 童雲蘭 譯
------------本文結束感謝您的閱讀------------