轉:CAS 和 AQS 原理

1. CAS

1.1 概念,什么是 CAS

CAS,compare and swap的縮寫,中文翻譯成比較并交換。CAS指令在Intel CPU上稱為CMPXCHG指令,它的作用是將指定內存地址的內容與所給的某個值相比,如果相等,則將其內容替換為指令中提供的新值,如果不相等,則更新失敗。

從內存領域來說這是樂觀鎖,因為它在對共享變量更新之前會先比較當前值是否與更新前的值一致,如果是,則更新,如果不是,則無限循環執行(稱為自旋),直到當前值與更新前的值一致為止,才執行更新。

1.2 CAS 的應用

CAS 有 3 個操作數,內存值 V,舊的預期值 A,要修改的新值 B。當且僅當預期值 A 和內存值 V 相同時,將內存值 V 修改為 B,否則什么都不做。

1.3 CAS 的缺點是什么

CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操作

  1. ABA問題。因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,
    那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。
    在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。

    從Java1.5開始JDK的atomic包里提供了一個類 AtomicStampedReference 來解決ABA問題。
    這個類的compareAndSet方法作用是首先檢查當前引用是否等于預期引用,并且當前標志是否等于預期標志,如果全部相等,
    則以原子方式將該引用和該標志的值設置為給定的更新值。

  1. 循環時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令那么效率會有一定的提升,
    pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,
    延遲的時間取決于具體實現的版本,在一些處理器上延遲時間是零。
    第二它可以避免在退出循環的時候因內存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
  1. 只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,
    但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,
    或者有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。
    從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進行CAS操作。

1.4 CAS 的原理

CAS 通過調用 JNI 的代碼實現的。JNI:java Native Interface 為 JAVA 本地調用,允許 java 調用其他語言。而compareAndSwapInt就是借助C來調用CPU底層指令實現的。

下面從分析比較常用的CPU(intel x86)來解釋CAS的實現原理。下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

結合對應于intel x86處理器的源代碼的片段分析,可知程序會根據當前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不需要lock前綴提供的內存屏障效果)。

2. AQS

具體細節看文章:

http://www.cnblogs.com/everSeeker/p/5582007.html , 主要講 reentrantlock

http://www.cnblogs.com/waterystone/p/4920797.html , 主要講 AQS 源碼,未讀完

http://www.lxweimin.com/p/6afaef97264a , 鎖的獲取和釋放流程,大致的講解。

2.1 概述

AbstractQueuedSynchronizer(簡稱AQS),隊列同步器,是用來構建鎖或者其他同步組建的基礎框架。該類主要包括:

  1. 模式分為共享和獨占。

  2. volatile int state,用來表示鎖的狀態。state = 0 表示鎖空閑,>0 表示鎖已被占用。

  3. FIFO雙向隊列,用來維護等待獲取鎖的線程。

image

獨占模式的鎖:ReentrantLock

共享模式的鎖:Semaphore,CountDownLatch

AQS 部分代碼說明如下:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    static final class Node {
        /** 共享模式,表示可以多個線程獲取鎖,比如讀寫鎖中的讀鎖 */
        static final Node SHARED = new Node();
        /** 獨占模式,表示同一時刻只能一個線程獲取鎖,比如讀寫鎖中的寫鎖 */
        static final Node EXCLUSIVE = null;

        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
    }

    /** AQS類內部維護一個FIFO的雙向隊列,負責同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等
        構造成一個節點Node并加入同步隊列;當同步狀態釋放時,會把首節點中線程喚醒,使其再次嘗試同步狀態 */
    private transient volatile Node head;
    private transient volatile Node tail;

    /** 狀態,主要用來確定lock是否已經被占用;在ReentrantLock中,state=0表示鎖空閑,>0表示鎖已被占用;可以自定義,改寫tryAcquire(int acquires)等方法即可  */
    private volatile int state;
}

這里主要說明下雙向隊列,通過查看源碼分析,隊列是這個樣子的:

head -> node1 -> node2 -> node3(tail)

注意:head初始時是一個空節點(所謂的空節點意思是節點中沒有具體的線程信息),之后表示的是獲取了鎖的節點。因此實際上head->next(即node1)才是同步隊列中第一個可用節點。

AQS的設計基于模版方法模式,使用者通過繼承AQS類并重寫指定的方法,可以實現不同功能的鎖。可重寫的方法主要包括:

image

不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至于具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:

  • isHeldExclusively():該線程是否正在獨占資源。只有用到condition才需要去實現它。

  • tryAcquire(int):獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false。

  • tryRelease(int):獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false。

  • tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩余可用資源;正數表示成功,且有剩余資源。

  • tryReleaseShared(int):共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。

一般來說,自定義同步器要么是獨占方法,要么是共享方式,他們也只需實現tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可。但AQS也支持自定義同步器同時實現獨占和共享兩種方式,如ReentrantReadWriteLock。

以ReentrantLock為例,state初始化為0,表示未鎖定狀態。A線程lock()時,會調用tryAcquire()獨占該鎖并將state+1。此后,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)為止,其它線程才有機會獲取該鎖。當然,釋放鎖之前,A線程自己是可以重復獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多么次,這樣才能保證state是能回到零態的。

再以CountDownLatch以例,任務分為N個子線程去執行,state也初始化為N(注意N要與線程個數一致)。這N個子線程是并行執行的,每個子線程執行完后countDown()一次,state會CAS減1。等到所有子線程都執行完后(即state=0),會unpark()主調用線程,然后主調用線程就會從await()函數返回,繼續后余動作。

兩個重要的狀態

1. AQS的state

state可以理解有多少線程獲取了資源,即有多少線程獲取了鎖,初始時state=0表示沒有線程獲取鎖。

獨占鎖時,這個值通常為1或者0,如果獨占鎖可重入時,即一個線程可以多次獲取這個鎖時,每獲取一次,state就加1。一旦有線程想要獲得鎖,就可以通過對state進行CAS增量操作,即原子性的增加state的值,其他線程發現state不為0,這時線程已經不能獲得鎖(獨占鎖),就會進入AQS的隊列中等待。釋放鎖是仍然是通過CAS來減小state的值,如果減小到0就表示鎖完全釋放(獨占鎖)

Node 中的waitStatus

Node的正常狀態是0。對于處在隊列中的節點來說,前一個節點有喚醒后一個節點的任務,所以對與當前節點的前一個節點來說,如果waitStatus > 0, 則節點處于cancel狀態,應踢出隊列,如果waitStatus = 0, 則將waitStatus改為-1(signal)。因此隊列中節點的狀態應該為-1,-1,-1,0

2.2 源碼詳解

acquire(int)

此方法是獨占模式下線程獲取共享資源的頂層入口。如果獲取到資源,線程直接返回,否則進入等待隊列,直到獲取到資源為止,且整個過程忽略中斷的影響。這也正是lock()的語義,當然不僅僅只限于lock()。獲取到資源后,線程就可以去執行其臨界區代碼了。下面是acquire()的源碼:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

函數流程如下

  1. 調用自定義同步器的tryAcquire()嘗試直接去獲取資源,如果成功則直接返回

  2. 沒成功,則addWaiter()將該線程加入等待隊列的尾部,并標記為獨占模式

  3. acquireQueued()使線程在等待隊列中休息,有機會時(輪到自己,會被unpark())會去嘗試獲取資源。獲取到資源后才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false

  4. 如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源后才再進行自我中斷selfInterrupt(),將中斷補上

流程圖:

image

release(int)

此方法是獨占模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果徹底釋放了(即state=0),它會喚醒等待隊列里的其他線程來獲取資源。這也正是unlock()的語義,當然不僅僅只限于unlock()。下面是release()的源碼:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;//找到頭結點
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//喚醒等待隊列里的下一個線程
        return true;
    }
    return false;
}

release()是根據tryRelease()的返回值來判斷該線程是否已經完成釋放掉資源了,如果已經徹底釋放資源(state=0),要返回true,否則返回false。

acquireShared(int)

此方法是共享模式下線程獲取共享資源的頂層入口。它會獲取指定量的資源,獲取成功則直接返回,獲取失敗則進入等待隊列,直到獲取到資源為止,整個過程忽略中斷。下面是acquireShared()的源碼:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

這里tryAcquireShared()依然需要自定義同步器去實現。但是AQS已經把其返回值的語義定義好了:負值代表獲取失敗;0代表獲取成功,但沒有剩余資源;正數表示獲取成功,還有剩余資源,其他線程還可以去獲取。所以這里acquireShared()的流程就是:

  1. tryAcquireShared()嘗試獲取資源,成功則直接返回。

  2. 失敗則通過doAcquireShared()進入等待隊列park(),直到被unpark()/interrupt()并成功獲取到資源才返回。整個等待過程也是忽略中斷的。

跟獨占模式比,還有一點需要注意的是:當前線程獲取資源成功后,如果還有剩余資源,那么還會喚醒后面的線程來嘗試獲取資源。

releaseShared()

此方法是共享模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果徹底釋放了(即state=0),它會喚醒等待隊列里的其他線程來獲取資源。下面是releaseShared()的源碼:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {//嘗試釋放資源
        doReleaseShared();//喚醒后繼結點
        return true;
    }
    return false;
}

跟獨占模式下的release()相似,但有一點稍微需要注意:獨占模式下的tryRelease()在完全釋放掉資源(state=0)后,才會返回true去喚醒其他線程,這主要是基于可重入的考量;而共享模式下的releaseShared()則沒有這種要求,一是共享的實質--多線程可并發執行;二是共享模式基本也不會重入吧(至少我還沒見過),所以自定義同步器可以根據需要決定返回值。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容