大家都知道,多線程下操作共享變量,會出現所謂的“線程安全問題”從而不能得到我們預期的結果,爲了解決這種問題,在早期的JDK版本中,提供的
synchronized
關鍵字來解決這種線程安全問題,而在JDK1.5以後的java.util.concurrent
包中,裏面大量使用了一種叫CAS的技術,提供了一種不用synchronized
的前提下解決線程安全問題的方案。本文將從AtomicInteger包入手,講解CAS的原理和使用、以及CAS可能出現的問題
先來一個小demo
不想上來就貼上來千篇一律的各種概念,懂得人我不說也懂,不懂得看完概念依然不懂,用碼說話,先看個小demo,開五個線程,每個線程累計1000次操作共享變量,共享變量分別使用int和基於CAS的AtomicInteger
public class AtomicTest {
int count = 0;
AtomicInteger atomicInteger = new AtomicInteger(0);
// 加1操作
public void add() {
count++;
atomicInteger.addAndGet(1);
}
public static void main(String[] args) throws InterruptedException {
AtomicTest atomicTest = new AtomicTest();
List<Thread> threads = new ArrayList<>();
// 開5個線程
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
// 每個線程執行1000次add方法
for (int num = 0; num < 1000; num++) {
atomicTest.add();
}
});
threads.add(thread);
thread.start();
}
// 保證上面開啓的五個線程執行在main線程之前
for (Thread thread : threads) {
thread.join();
}
System.out.println("count-->" + atomicTest.count);
System.out.println("atomic-->" + atomicTest.atomicInteger);
}
}
結果可能每次都不一樣,但是使用AtomicInteger的值每次必是5000
count-->3670
atomic-->5000
Process finished with exit code 0
點進源碼看一下
(基於JDK1.8,不用版本可能稍有不同)
點進atomicInteger.addAndGet(1);
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the updated value
*/
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
只有一行代碼,然後調用了unsafe的getAndAddInt,傳參分別是當前對象引用,valueOffset和我們要增加的值delta,valueOffset我們一會詳細說一下,現在先點到unsafe.getAndAddInt(this, valueOffset, delta)
中看一下
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
我們看到,進入了一個叫Unsafe
的類中,首先先調取this.getIntVolatile(var1, var2);
,這是一個native方法,通過傳入的var1(也就是上面AtomicInteger的this)和var2,從內存中獲取到最新的值,然後執行compareAndSwapInt(var1, var2, var5, var5 + var4)
方法,就是我們的主角CAS(Compare and Swap)
,依然是native方法,實現原理是拿當前獲取到的值(var5)通內存的值比較,如果值相同,則將內存的值更新爲新的值(var5+var4,var4也就是我們上面傳入的delta),如果與內存中的值不同,則繼續執行do
中的邏輯,獲取最新的var5並繼續執行compareAndSwap操作,直至成功。
其實CAS直至JDK1.5才被廣泛使用的原因是,CAS是需要硬件支持的,隨着處理器的發展,逐漸提供了將一系列操作原子操作的指令,其中CAS底層使用的就是CMPXCHG
指令。
回到上面這段代碼,看到最後return的值爲var5,並不是執行操作後最新的值,當返回var5後,addAndGet(int delta)
中,再加上delta,其實大家想一想,這樣操作,雖然能保證AtomicInteger內部每次操作都是原子性的,卻不能保證每次調用addAndGet
方法返回的數據是順序的,換句話說,它能保證結果的最終一致性,但是並不能保證每條線程中間狀態的一致性,這也是爲什麼atomic包的類適合計數,但並不適合做例如生成訂單號等要求強順序的業務操作。
重點說下valueOffset
回過頭來,看下剛剛還沒解釋的valueOffset
,是個啥。valueOffset
,存於AtomicInteger的成員變量中,在靜態代碼塊中初始化
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
然後整個AtomicInteger中並沒有修改它的地方。說明這個值在整個JVM中是唯一的,一個唯一的值,還被頻繁傳到各種native方法中,有什麼意義的?當我們創建了一個對象之後,在內存中開闢了一片空間,但是這個對象的這片空間中,也是存放了各種數據的,這些對象怎麼存放樣式,被稱作“佈局”,而這個valueOffset
,就是獲取的一個字段在這個佈局中的一個相對位置,叫着“偏移量”,根據上面代碼我們看到獲取的是value
,這樣,通過當前對象和value的偏移量,可以在內存中快速的定位到value
的值。
說到 CAS不得不說的ABA問題
CAS看起來不會有任何問題,完美解決了多線程下數據安全問題,但是可能有這樣一個場景
線程1
:
獲取內存中的值,爲A
然後被掛起線程2
:
獲取內存中的值,爲A
修改A爲B
修改B爲A
執行完畢線程1
:
獲取到的值A與內存中比對一致,可以操作
上述場景就出現了ABA問題,關於ABA問題會造成的問題,感興趣的小夥伴可以查一查深入瞭解,要是基於我們正常使用的常用操作,能保證結果的正確,並不會對我們造成什麼影響。如果非要規避ABA的場合,可以使用AtomicStampedReference
,通過內部維護的一個時間戳來保證,比如上面線程2的操作,可理解爲
修改A爲B(2B)
修改B爲A(3A)
此時線程1執行,獲取到的A與當前內存中的3A就有差異了。
雖然能規避ABA問題,但是額外的開銷勢必會影響性能,而使用這種規避ABA問題的CAS實現,性能有沒有使用synchronized
高呢?可以根據實際業務場景和配置,做一個性能對比來權衡。