CAS即Compare And Swap
對比交換,區別於悲觀鎖,藉助CAS能夠實現區別於synchronized獨佔鎖的一種樂觀鎖,被普遍應用在各大編程語言之中。Java JUC底層大量使用了CAS,能夠說java.util.concurrent
徹底是創建在CAS之上的。可是CAS也有相應的缺點,諸如ABA
、cpu使用率高
等問題沒法避免。java
CAS總共有3個操做數,當前內存值V,舊的預期值A,要修改的新值N。當且僅當A和V相同時,將V修改成N,不然什麼都不作。程序員
咱們都知道,java提供了一些列併發安全的原子操做類,如AtomicInteger
、AtomicLong
.下面咱們拿AtomicInteger
爲例分析其源碼實現。編程
// 一、獲取UnSafe實例對象,用於對內存進行相關操做
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); }
}
// 四、具體值,使用volatile保證可見性
private volatile int value;
複製代碼
從上面代碼中咱們能夠看出,AtomicInteger
中依賴於一個叫Unsafe
的實例對象,咱們都知道,java語言屏蔽了像C++那樣直接操做內存的操做,程序員不需手動管理內存,但話說回來,java仍是開放了一個叫Unsafe
的類直接對內存進行操做,由其名字能夠看出,使用Unsafe
中的操做是不安全的,要當心謹慎。安全
valueOffset
是對象的內存偏移地址,經過Unsafe
對象進行初始化,有一點須要注意的是,對於給定的某個字段都會有相同的偏移量,同一類中的兩個不一樣字段永遠不會有相同的偏移量。也就是說,只要對象不死,這個偏移量就永遠不會變,能夠想象,CAS所依賴的第一個參數(內存地址值)正是經過這個地址偏移量進行獲取的。bash
value
屬於共享資源,藉助volatile
保證內存可見性,關於volatile
的簡單分析,能夠參考併發
// 一、獲取並增長delta
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 二、加一
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
複製代碼
上面兩個方法依賴下面Unsafe
類中的getAndAddInt
操做,藉助openjdk
提供的Unsafe
源碼,咱們看下其實現:ide
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
// 一、不斷的循環比較,直到CAS操做成功返回
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
複製代碼
從上面能夠看出,本質上CAS使用了自旋鎖進行自旋,直到CAS操做成功,若是很長一段時間都沒有操做成功,那麼將一直自旋下去。高併發
從第1、二節能夠看出,CAS在java中的實現本質上是使用Unsafe
類提供的方法獲取對象的內存地址偏移量,進而經過自旋實現的。CAS的優勢很明顯,那就是區別於悲觀策略,樂觀策略在高併發下性能表現更好,固然CAS也是有缺點的,主要有相似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執行失敗了。
經過第二節分析能夠得知,CAS本質上是經過自旋來判斷是否更新的,那麼問題來了,若是屢次舊預期值不等於內存值的狀況,那麼這個自旋將會自旋下去,而自旋太久將會致使CPU利用率變高。
從第二節能夠看出,只是單純對單個共享對象進行CAS操做,保證了其更新獲取的原子性,沒法對多個共享變量同時進行原子操做。這是CAS的侷限所在,但JDK提供同時了AtomicReference類來保證引用對象之間的原子性,能夠把多個變量放在一個對象裏來進行CAS操做。