原文地址:【死磕Java併發】—-深刻分析CASjava
CAS,Compare And Swap,即比較並交換。Doug lea大神在同步組件中大量使用CAS技術鬼斧神工地實現了Java多線程的併發操做。整個AQS同步組件、Atomic原子類操做等等都是以CAS實現的,甚至ConcurrentHashMap在1.8的版本中也調整爲了CAS+Synchronized。能夠說CAS是整個JUC的基石。緩存
在CAS中有三個參數:內存值V、舊的預期值A、要更新的值B,當且僅當內存值V的值等於舊的預期值A時纔會將內存值V的值修改成B,不然什麼都不幹。其僞代碼以下多線程
if(this.value == A){
this.value = B
return true;
}else{
return false;
}
複製代碼
JUC下的atomic類都是經過CAS來實現的,下面就以AtomicInteger爲例來闡述CAS的實現。以下:併發
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
複製代碼
CAS能夠保證一次的讀-改-寫操做是原子操做,在單處理器上該操做容易實現,可是在多處理器上實現就有點兒複雜了。 CPU提供了兩種方法來實現多處理器的原子操做:ide
總線加鎖:總線加鎖就是就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其餘處理器的請求將被阻塞住,那麼該處理器能夠獨佔使用共享內存。可是這種處理方式顯得有點兒霸道,不厚道,他把CPU和內存之間的通訊鎖住了,在鎖按期間,其餘處理器都不能其餘內存地址的數據,其開銷有點兒大。因此就有了緩存加鎖。this
緩存加鎖:其實針對於上面那種狀況咱們只須要保證在同一時刻對某個內存地址的操做是原子性的便可。緩存加鎖就是緩存在內存區域的數據若是在加鎖期間,當它執行鎖操做寫回內存時,處理器不在輸出LOCK#信號,而是修改內部的內存地址,利用緩存一致性協議來保證原子性。緩存一致性機制能夠保證同一個內存區域的數據僅能被一個處理器修改,也就是說當CPU1修改緩存行中的i時使用緩存鎖定,那麼CPU2就不能同時緩存了i的緩存行。atom
CAS雖然高效地解決了原子操做,可是仍是存在一些缺陷的,主要表如今三個方法:循環時間太長、只能保證一個共享變量原子操做、ABA問題。spa
循環時間太長線程
若是CAS一直不成功呢?這種狀況絕對有可能發生,若是自旋CAS長時間地不成功,則會給CPU帶來很是大的開銷。在JUC中有些地方就限制了CAS自旋的次數,例如BlockingQueue的SynchronousQueue。code
只能保證一個共享變量原子操做
看了CAS的實現就知道這隻能針對一個共享變量,若是是多個共享變量就只能使用鎖了,固然若是你有辦法把多個變量整成一個變量,利用CAS也不錯。例如讀寫鎖中state的高地位。
ABA問題
CAS須要檢查操做值有沒有發生改變,若是沒有發生改變則更新。可是存在這樣一種狀況:若是一個值原來是A,變成了B,而後又變成了A,那麼在CAS檢查的時候會發現沒有改變,可是實質上它已經發生了改變,這就是所謂的ABA問題。對於ABA問題其解決方案是加上版本號,即在每一個變量都加上一個版本號,每次改變時加1,即A —> B —> A,變成1A —> 2B —> 3A。
CAS的ABA隱患問題,解決方案則是版本號,Java提供了AtomicStampedReference來解決。AtomicStampedReference經過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題。
AtomicStampedReference的compareAndSet()方法定義以下:
/** * @param expectedReference the expected value of the reference * @param newReference the new value for the reference * @param expectedStamp the expected value of the stamp * @param newStamp the new value for the stamp * @return {@code true} if successful */
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) == 當前引用(current.reference)而且預期的標識(expectedStamp) == 當前標識(current.stamp),若是更新後的引用和標誌和當前的引用和標誌相等則直接返回true,不然經過Pair生成一個新的pair對象與當前pair CAS替換。Pair爲AtomicStampedReference的內部類,主要用於記錄引用和版本戳信息(標識),定義以下:
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
複製代碼
Pair記錄着對象的引用和版本戳,版本戳爲int型。同時Pair是一個不可變對象,其全部屬性所有定義爲final
,對外提供一個of方法,該方法返回一個新建的Pari對象。pair對象定義爲volatile,保證多線程環境下的可見性。在AtomicStampedReference中,大多方法都是經過調用Pair的of方法來產生一個新的Pair對象,而後賦值給變量pair。如set方法:
public void set(V newReference, int newStamp) {
Pair<V> current = pair;
if (newReference != current.reference || newStamp != current.stamp)
this.pair = Pair.of(newReference, newStamp);
}
複製代碼
咱們定義兩個線程,線程1負責將100 —> 110 —> 100,線程2執行 100 —>120,看二者之間的區別。
public class AtomicStampedReferenceExample {
private static AtomicInteger atomicInteger = new AtomicInteger(100);
private static AtomicStampedReference atomicStampedReference =
new AtomicStampedReference(100,1);
public static void main(String[] args) throws InterruptedException {
//AtomicInteger
Thread at1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.compareAndSet(100,110);
atomicInteger.compareAndSet(110,100);
}
});
Thread at2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2); // at1,執行完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100,120));
}
});
at1.start();
at2.start();
at1.join();
at2.join();
//AtomicStampedReference
Thread tsf1 = new Thread(new Runnable() {
@Override
public void run() {
try {
//讓 tsf2先獲取stamp,致使預期時間戳不一致
TimeUnit.SECONDS.sleep(1);
} 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();
}
}
複製代碼
若是讀完以爲有收穫的話,歡迎點贊、關注、加公衆號【牛覓技術】,查閱更多精彩歷史!!!: