一、CAS簡介
CAS即Compare And Swap
對比交換,區別於悲觀鎖,藉助CAS可以實現區別於synchronized獨佔鎖的一種樂觀鎖,被廣泛應用在各大程式語言之中。Java JUC底層大量使用了CAS,可以說java.util.concurrent
完全是建立在CAS之上的。但是CAS也有相應的缺點,諸如ABA
、cpu使用率高
等問題無法避免。
CAS總共有3個運算元,當前記憶體值V,舊的預期值A,要修改的新值N。當且僅當A和V相同時,將V修改為N,否則什麼都不做。
二、CAS原始碼分析
我們都知道,java提供了一些列併發安全的原子操作類,如AtomicInteger
、AtomicLong
.下面我們拿AtomicInteger
為例分析其原始碼實現。
// 1、獲取UnSafe例項物件,用於對記憶體進行相關操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 2、記憶體偏移量
private static final long valueOffset;
static {
try {
// 3、初始化地址偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 4、具體值,使用volatile保證可見性
private volatile int value;
複製程式碼
從上面程式碼中我們可以看出,AtomicInteger
中依賴於一個叫Unsafe
的例項物件,我們都知道,java語言遮蔽了像C++那樣直接操作記憶體的操作,程式設計師不需手動管理記憶體,但話說回來,java還是開放了一個叫Unsafe
的類直接對記憶體進行操作,由其名字可以看出,使用Unsafe
中的操作是不安全的,要小心謹慎。
valueOffset
是物件的記憶體偏移地址,通過Unsafe
物件進行初始化,有一點需要注意的是,對於給定的某個欄位都會有相同的偏移量,同一類中的兩個不同欄位永遠不會有相同的偏移量。也就是說,只要物件不死,這個偏移量就永遠不會變,可以想象,CAS所依賴的第一個引數(記憶體地址值)正是通過這個地址偏移量進行獲取的。
value
屬於共享資源,藉助volatile
保證記憶體可見性,關於volatile
的簡單分析,可以參考
// 1、獲取並增加delta
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 2、加一
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
複製程式碼
上面兩個方法依賴下面Unsafe
類中的getAndAddInt
操作,藉助openjdk
提供的Unsafe
原始碼,我們看下其實現:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
// 1、不斷的迴圈比較,直到CAS操作成功返回
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
複製程式碼
從上面可以看出,本質上CAS使用了自旋鎖進行自旋,直到CAS操作成功,如果很長一段時間都沒有操作成功,那麼將一直自旋下去。
三、CAS的缺點
從第一、二節可以看出,CAS在java中的實現本質上是使用Unsafe
類提供的方法獲取物件的記憶體地址偏移量,進而通過自旋實現的。CAS的優點很明顯,那就是區別於悲觀策略,樂觀策略在高併發下效能表現更好,當然CAS也是有缺點的,主要有類似ABA
、自旋時間過長
、只能保證一個共享變數原子操作
三大問題,下面我們一一分析。
1、ABA
什麼是ABA呢?簡單的說,就是有兩個執行緒,執行緒A和執行緒B,對於同一個變數X=0,A準備將X置為10,按照CAS的步驟,首先會從記憶體讀取值舊的預期值0,然後比較,最後置為10,但就在A讀取完X=0後,還沒來得及比較和賦值,此時執行緒B完成了X=0 -> X=10 -> X=0
這3個操作,隨後A繼續執行比較,發現此時記憶體的值依舊是0,最後CAS執行成功。雖然過程和結果沒有問題,但是A比較時的0已經不是最初那個0了,有種被偷樑換柱的感覺。
下面程式碼舉例演示ABA
問題,執行緒1模擬將變數從100->110->100,執行緒2執行100->120,最後看下輸出:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
*
* @Author jiawei huang
* @Since 2020年1月17日
* @Version 1.0
*/
public class ABATest {
// 初始值為100
private static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
// AtomicInteger實現 100->110->100
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.compareAndSet(100, 110);
atomicInteger.compareAndSet(110, 100);
}
});
// 實現 100->120
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 這裡模擬執行緒1執行完畢,偷樑換柱成功
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 下面依舊返回true
System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100, 120));
}
});
t1.start();
t2.start();
}
}
複製程式碼
輸出結果為:
AtomicInteger:true
複製程式碼
可見執行緒2中的CAS也執行成功了,那麼如何解決這個問題呢?解決方案是通過版本號,Java提供了AtomicStampedReference
來解決。AtomicStampedReference
通過包裝[E,Integer]
的元組來對物件標記版本戳stamp
,從而避免ABA
問題。
/*
* Copyright (C) 2011-2019 DL
*
* All right reserved.
*
* This software is the confidential and proprietary information of DL of China.
* ("Confidential Information"). You shall not disclose such Confidential
* Information and shall use it only in accordance with the argeements
* reached into with DL himself.
*
*/
package com.algorithm.leetcode.linkedlist;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
*
* @Author jiawei huang
* @Since 2020年1月17日
* @Version 1.0
*/
public class ABATest {
// 初始值100,版本號1
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100, 1);
public static void main(String[] args) throws InterruptedException {
// AtomicStampedReference實現
Thread tsf1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 讓 tsf2先獲取stamp,導致預期時間戳不一致
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 預期引用:100,更新後的引用:110,預期標識getStamp() 更新後的標識getStamp() + 1
atomicStampedReference.compareAndSet(100, 110, atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
atomicStampedReference.compareAndSet(110, 100, atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
}
});
Thread tsf2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedReference.getStamp();
try {
TimeUnit.SECONDS.sleep(2); // 執行緒tsf1執行完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(
"AtomicStampedReference:" + atomicStampedReference.compareAndSet(100, 120, stamp, stamp + 1));
}
});
tsf1.start();
tsf2.start();
}
}
複製程式碼
輸出結果:
AtomicStampedReference:false
複製程式碼
可以看出執行緒1執行失敗了。
2、自旋時間過長
通過第二節分析可以得知,CAS本質上是通過自旋來判斷是否更新的,那麼問題來了,如果多次舊預期值不等於記憶體值的情況,那麼這個自旋將會自旋下去,而自旋過久將會導致CPU利用率變高。
3、只能保證一個共享變數原子操作
從第二節可以看出,只是單純對單個共享物件進行CAS操作,保證了其更新獲取的原子性,無法對多個共享變數同時進行原子操作。這是CAS的侷限所在,但JDK提供同時了AtomicReference類來保證引用物件之間的原子性,可以把多個變數放在一個物件裡來進行CAS操作。