一、什麼是CAS
CAS即比較並替換,是一種輕量級鎖,一般用於併發量不大的場景,CAS機制中用了3個變量:內存值V,舊的預期值A,要修改的新值B;只有當內存中的值和舊的預期值相等的情況下才更新值爲B,否則該線程會一直自旋等待,下面我們用大白話來解釋CAS。
二、CAS 應用
想象一下假如現在我們有2個線程,對共享變量進行i++操作,如果不加鎖會出現什麼情景呢,先看一個代碼
private static int count = 0;
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
for(int i=0;i<2;i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<100;i++) {
count++;
}
latch.countDown();
}
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
這份代碼理想情況下,按照預期會輸出count=200,但是大多數情況下它的輸出都小於200,這是因爲i++操作不是原子的,這個時候我們可能會想到加鎖,先來看看加synchronizedpublic class JAVA的CAS機制 {
private static int count = 0;
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
for(int i=0;i<2;i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (JAVA的CAS機制.class) {
for(int i=0;i<100;i++) {
count++;
}
}
latch.countDown();
}
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
}
這份代碼會輸出count=200,因爲我們通過synchronized來保證每次只有一個線程對共享變量進行操作,雖然synchronized可以解決第一份代碼的問題,但是synchronized相當於一個悲觀鎖,每次只允許一個線程操作。那我們這裏要說的CAS是怎麼用呢,它是怎麼解決第一份代碼的問題呢,我們先看一段代碼
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
count.getAndIncrement();
}
latch.countDown();
}
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count.get());
}
這份代碼輸出一直都是count=200,這裏使用了JAVA原子包裏面提供的類atomicInteger,這是一個原子操作類,如果大家感興趣可以看看它的源代碼,底層就是使用了CAS機制來實現,CAS相當於synchronized的輕量級實現,是一個樂觀鎖,假如某個線程沒有更新到值,那麼它會一直自旋並不斷重試,直到最後更新值。在併發量不大的時候,建議大家使用CAS。
三、AtomicInteger源碼分析
JAVA的CAS機制大量用在了JAVA併發原子類裏面,這裏我們以AtomicInteger類來分析其具體實現
先來看定義
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
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;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(int newValue) {
value = newValue;
}
其中Unsafe類是CAS的核心實現類,JAVA通過它來訪問本地方法;而valueOffset則是變量在內存中的偏移地址,;而用valatile修飾得變量value保證了多線程內存可見性。
再看看getAndIncrement
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
調用unsafe.getAndInt,假設當前內存中變量值爲2,同時有2個線程來操作這個變量,假設A線程拿到了這個變量,準備修改爲3,B線程也準備修改這個變量,發現目前的值3和自己手裏的變量副本2不一樣,所以更新失敗,這就相當於使用了CAS四、CAS的問題
4.1 CPU消耗大
CAS線程會不停自旋,如果併發量大的話,將會不停重試,還不釋放CPU,極端情況下會耗光資源。
4.2 只能保證某個變量的原子操作
從第三份代碼我們看到,CAS只能保證單個變量的原子性,不能保證某個代碼塊或者某個對象的原子性(這裏可以用併發包裏面的原子更新類AtomicReference實現)
4.3 經典的ABA問題
就是說變量A在某個線程修改後成B後,另一個線程又將其修改爲A,導致其他線程誤認爲這個變量實際沒有變過,這就是經典的ABA問題,要解決這個,我們可以加一個版本號,每次比較的時候,不僅需要比較值,還需要看版本號是否一致,一致才修改,在JAVA併發包裏面AtomicStampedReference類就是用了版本號來解決ABA問題。