一、前言
今天花了點時間了解了一下JDK1.8中ConcurrentHashMap的實現,發現它實現的主要思想就是依賴於CAS機制。CAS機制是並發中比較重要的一個概念,所以今天這篇博客就來詳細介紹一下CAS機制以及Java中對CAS的適用。
二、正文
2.1 樂觀鎖與悲觀鎖
在講CAS之前,先來理解兩個概念,即樂觀鎖和悲觀鎖:
- 樂觀鎖:在並發下對數據進行修改時保持樂觀的態度,認為在自己修改數據的過程中,其他線程不會對同一個數據進行修改,所以不對數據加鎖,但是會在最終更新數據前,判斷一下這個數據有沒有被修改,若沒有被修改,才將它更新為自己修改的值;
- 悲觀鎖:在並發下對數據進行修改時保持悲觀的態度,認為在自己修改數據的過程中,其他線程也會對數據進行修改,所以在操作前會對數據加鎖,在操作完成后才將鎖釋放,而在釋放鎖之前,其他線程無法操作數據;
CAS其實就是樂觀鎖的一種實現方式,而悲觀鎖比較典型的就是Java中的synchronized。下面我就來詳細介紹一下CAS的相關概念。
2.2 什么是CAS?
CAS全稱compare and swap——比較並替換,它是並發條件下修改數據的一種機制,包含三個操作數:
- 需要修改的數據的內存地址(V);
- 對這個數據的舊預期值(A);
- 需要將它修改為的值(B);
CAS的操作步驟如下:
- 修改前記錄數據的內存地址V;
- 讀取數據的當前的值,記錄為A;
- 修改數據的值變為B;
- 查看地址V下的值是否仍然為A,若為A,則用B替換它;若地址V下的值不為A,表示在自己修改的過程中,其他的線程對數據進行了修改,則不更新變量的值,而是重新從步驟2開始執行,這被稱為自旋;
通過以上四個步驟對內存中的數據進行修改,就可以保證數據修改的原子性。CAS是樂觀鎖的一種實現,所以這里介紹的步驟和樂觀鎖的定義差不多,還是很好理解的。
2.3 Java中CAS的使用
Java中大量使用的CAS,比如,在java.util.concurrent.atomic包下有很多的原子類,如AtomicInteger、AtomicBoolean......這些類提供對int、boolean等類型的原子操作,而底層就是通過CAS機制實現的。比如AtomicInteger類有一個實例方法,叫做incrementAndGet,這個方法就是將AtomicInteger對象記錄的值+1並返回,與i++類似。但是這是一個原子操作,不會像i++一樣,存在線程不一致問題,因為i++不是原子操作。比如如下代碼,最終一定能夠保證num的值為200:
// 聲明一個AtomicInteger對象
AtomicInteger num = new AtomicInteger(0);
// 線程1
new Thread(()->{
for (int i = 0; i < 100; i++) {
// num++
num.incrementAndGet();
}
}).start();
// 線程2
new Thread(()->{
for (int i = 0; i < 100; i++) {
// num++
num.incrementAndGet();
}
}).start();
Thread.sleep(1000);
System.out.println(num);
我們看看incrementAndGet方法的源碼:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
這里使用了一個unsafe對象,而unsafe對象是什么呢?我們知道,Java並不能像C或C++一樣,直接操作內存,但是JVM為我們提供了一個后門,就是sun.misc.Unsafe類,這個類為我們實現了很多硬件級別的原子方法,當然,這些方法都是native方法,使用其他語言實現,而不是Java方法。而上面的另外一個變量valueOffset就是我們需要修改的變量在內存中的偏移量。也許上面這個方法並不能讓你感覺使用了CAS,那再看看下面這個方法:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
compareAndSet是AtomicInteger的另一個方法,它的作用就是給定一個預期的舊值expect,以及需要更新為的值update,若當前變量的值是expect,就將其修改為update,否則不修改(這不就是CAS的思想嗎)。而它底層調用了unsafe對象的compareAndSwapInt方法,從這個名字可以看出,它的實現使用的就是CAS。compareAndSwapInt的三個參數valueOffset、expect以及update,剛好對應了CAS操作的三個操作數。
2.4 CAS機制的ABA問題
CAS機制雖然簡單,但是也存在一些缺陷,其中比較典型的就是ABA問題。什么是ABA問題,我簡單介紹一下:
- 假設有三個線程
T1、T2和T3,它們都要對一個變量num的值進行修改,且使用的都是CAS機制進行同步,假設num的初始值為100; - 線程
T1首先讀取了num的值,將它記錄為舊預期A1 = 100,然后它想要將num的值修改為80,記錄B2 = 80,在執行num = B2前,線程發生了切換,切換到線程T2; - 假設
T2毫無阻礙地修改了num的值,將它從100修改為80,然后線程再度切換,T3開始執行; T3也是毫無阻礙地修改了num,將它從80重新修改為100,線程再次切換回T1;T1從上次運行的斷點恢復,也就是准備用B1的值覆蓋num,但是由於CAS機制,它需要先檢測num的值是否等於它記錄的預期值A1,然后它發現A1 = num = 100,認為num沒有被修改過,於是用B1覆蓋了num;
上面這種情況就是CAS的ABA問題:一個變量被修改,但是又被改了回去,在CAS機制中,將無法察覺這種錯誤的現象。在線程T1被中斷的過程中,num的值被修改,按照CAS的原則,T1應該放棄對num的修改,從頭開始執行。有人可能想問,修改回去之后,不就和沒修改一樣嗎,有什么影響呢?其實我也不知道有什么影響.....找遍網上的博客,舉的例子都是在扯淡,誤人子弟(看的最多的例子就是那個銀行取錢的,一個人亂寫,其他人亂copy,真心服了)。如果有知道ABA問題影響的朋友,麻煩告知一下。
對於ABA問題的解決方案也非常簡單,那就是再添加一個變量——版本號。每個變量都加上一個版本號,在它被修改時,也同步修改版本號,而CAS操作在修改前記錄版本號,若在最后更新變量時,記錄的版本號與當前版本號一致,表示沒有被修改,可直接更新。
2.5 CAS的優缺點以及適用場景
(1)優點
前面也提到過,CAS是一種樂觀鎖,其優點就是不需要加鎖就能進行原子操作;
(2)缺點
CAS的缺點主有兩點:
CAS機制只能用在對某一個變量進行原子操作,無法用來保證多個變量或語句的原子性(synchronized可以);- 假設在修改數據的過程中經常與其他線程修改沖突,將導致需要多次的重新嘗試;
(3)適用場景
由上面分析的優缺點可以看出,CAS適用於並發沖突發生頻率較低的場合,而對於並發沖突較頻繁的場合,CAS由於不斷重試,反倒會降低效率。
三、總結
CAS是一種在並發下實現原子操作的機制,但是只能用來保證一個變量的原子性,適用於並發沖突頻率較低的場合。
四、參考
推薦兩篇描述CAS的博客,這兩篇博客通過漫畫對CAS進行了非常詳細的描述:
