一、爲什麼值和預期不一樣?
我們先來看下下面的這段 Java 程序,開啓十個線程,每個線程進行 number++ 操作 1000 次,最終輸出的值大小應該爲 10000:
public void addNumber(){
number++;
}
for (int i = 1; i <=10 ; i++) {
new Thread(()->{
for (int j = 1; j <=1000; j++) {
data.addNumber();
}
},String.valueOf(i)).start();
}
當我們打印出最終的 number 的值的時候發現,每一次的值都小於預期的 10000 。這是因爲 number++ 並非是一步操作,當執行它時會分爲三條指令:① 獲取到原始值;② 對原始值進行加一操作得到新值;③ 將新值寫回內存。在併發較高的情況下,當兩個線程同時獲取到舊的值之後,就會產生寫入的值相同的狀況,造成總和總比預期值小的後果。
爲了解決這個問題,我們可以給 addNumber()
方法加上 synchronized
修飾解決。synchronized
屬於悲觀鎖,一次只允許一個線程進行 number++
的操作,雖然這樣能夠解決併發問題,但是在此處的效率並不高。由此我們可以使用一種樂觀鎖,樂觀鎖的含義是假設沒有發生衝突,那麼就正好可以進行某項操作,如果要是發生衝突了,那就重試直到成功,其最常見的就是CAS
。對 addNumber()
方法進行以下修改便可以避免併發值重複的問題。
AtomicInteger atomicInteger =new AtomicInteger();
public void addNumber(){
atomicInteger.getAndIncrement();
}
這裏邊使用到了java.util.concurrent.atomic
包下的AtomicInteger
來解決原子性問題。
二、CAS是什麼?
在上面我們使用到了 Java 的java.util.concurrent.atomic
包,這個包便借用了 CAS 來實現了區別於synchronized
同步鎖的一種樂觀鎖。
那麼 CAS 是什麼呢?CAS其實就是Compare And Swap 的簡寫,它是一條 CPU 併發原語。原語的執行必須是連續的,在執行過程中不允許中斷,也就是說CAS是一條原子指令,不會造成所謂的數據不一致的問題。我們跟蹤atomicInteger.getAndIncrement();
這條語句可以得到以下的內容:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
由此可以發現它的實現藉助了一個叫做Unsafe
的類。Unsafe
類是 Java 中用於直接操作內存數據的一個類(類似於C語言中的指針操作),其中包含很多的本地方法(native)。
以getAndAddInt(Object var1, long var2, int var4)
方法爲例,展示其實如何實現的保證原子性。方法的實現如下:
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;
}
該方法中包含三個參數,分別代表的意思是:
① 當前對象;
② 該變量值在內存中的偏移地址;
③ 需要增加的值大小。
在這段代碼中,有一個叫做compareAndSwapInt
的方法,public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
,該方法共包含四個參數,返回值爲布爾型,使用當前對象的當前值與 var5 進行比較,如果相同則更新值返回 true,失敗返回 false,參數含義如下。
Object var1:代表當前對象;
long var2:代表內存偏移量,相當於對象值的引用地址;
int var4:代表期望值,使用期望值和當前對象中的值進行比較;
int var5:代表要交換的值。
該方法中使用了自旋鎖以保證其原子性。假設主內存值爲 v 等於10,此時有 T1、T2兩個線程進入到該方法,根據 Java 內存模型(JMM)我們可以知道,線程 T1 和線程 T2 都會將主內存的值10拷貝到自己的工作內存。
1、當線程 T1 和線程 T2 都通過getIntVolatile(var1, var2)
賦值給了變量 var5 之後,線程 T1 被掛起;
2、線程 T2 調用方法compareAndSwapInt
,因爲當中的期望值 var5 和當前主內存值相同,比較成功,更新當前內存的值爲 11,返回 true,退出循環;
3、線程 T1 被喚醒,在執行compareAndSwapInt
方法的時候,由於當前內存的值以及爲11,和 工作內存 var5 的值10不同了,所以比較不成功,返回 false,繼續執行循環;
4、線程 T1 重新從主內存獲取當前的最新值11賦值給 var5;
5、線程 T1 繼續進行比較,若此時沒有其他線程對主內存的進行修改,比較更新成功 ,退出循環;否則繼續執行步驟4。
流程圖如下所示:
雖然CAS沒有加鎖保證了一致性,併發性有所提高 ,但是也產生了一系列的問題,比如循環時間長開銷大、只能保證一個共享變量的原子操作、會產生ABA問題。
三、ABA 問題是什麼?
使用 CAS 會產生 ABA 問題,這是因爲 CAS 算法是在某一時刻取出內存值然後在當前的時刻進行比較,中間存在一個時間差,在這個時間差裏就可能會產生 ABA 問題。
ABA 問題的過程是當有兩個線程 T1 和 T2 從內存中獲取到值A,線程 T2 通過某些操作把內存 值修改爲B,然後又經過某些操作將值修改爲回值A,T2退出。線程 T1 進行操作的時候 ,使用預期值同內存中的值比較,此時均爲A,修改成功退出。但是此時的A以及不是原先的A了,這就是 ABA 問題,如下圖。
四、如何解決 ABA 問題?
解決這個問題可以使用添加版本號的方式。我們可以使用 Java 中的提供的類AtomicStampedReference
進行操作,其中的compareAndSet
方法如下:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
這個方法包含了四個參數,expectedReference
代表的是期望被修改的值,newReference
代表的是新的值,expectedStamp
代表期望被修改的版本號,newStamp
代表新的版本號。只有當預期值和均當前內存值相同時纔會修改成功。ABA 問題的完整示例以及解決代碼如下:
public class ABADemo {
private static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",版本號爲" + stamp + ",值是" + stampedReference.getReference());
//暫停1秒鐘t1線程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",版本號爲" + stampedReference.getStamp() + ",值是" + stampedReference.getReference());
stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",版本號爲" + stampedReference.getStamp() + ",值是" + stampedReference.getReference());
System.out.println("線程t1已完成1次ABA操作~~~~~");
}, "t1").start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",版本號爲" + stamp + ",值是" + stampedReference.getReference());
//線程2暫停3秒,保證線程1完成1次ABA
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = stampedReference.compareAndSet(100, 6666, stamp, stamp + 1);
System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",修改成功否:" + result + ",最新版本號" +
stampedReference.getStamp() + ",最新的值:" + stampedReference.getReference());
}, "t2").start();
}
}