深入淺出java同步器AQS

簡書 占小狼
轉載請注明原創出處,謝謝!

前言

在java.util.concurrent.locks包中有很多Lock的實現類,常用的有ReentrantLock、ReadWriteLock(實現類ReentrantReadWriteLock),內部實現都依賴AbstractQueuedSynchronizer類,接下去讓我們看看Doug Lea大神是如何使用一個普通類就完成了代碼塊的并發訪問控制。為了方便,本文中使用AQS代替AbstractQueuedSynchronizer。

定義

public abstract class AbstractQueuedSynchronizer extends
    AbstractOwnableSynchronizer implements java.io.Serializable { 
    //等待隊列的頭節點
    private transient volatile Node head;
    //等待隊列的尾節點
    private transient volatile Node tail;
    //同步狀態
    private volatile int state;
    protected final int getState() { return state;}
    protected final void setState(int newState) { state = newState;}
    ...
}

隊列同步器AQS是用來構建鎖或其他同步組件的基礎框架,內部使用一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作,其中內部狀態state,等待隊列的頭節點head和尾節點head,都是通過volatile修飾,保證了多線程之間的可見。

在深入實現原理之前,我們先看看內部的FIFO隊列是如何實現的。

static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
        ...
    }

先來一張形象的圖(該圖其實是網上找的)


FIFO.png

黃色節點是默認head節點,其實是一個空節點,我覺得可以理解成代表當前持有鎖的線程,每當有線程競爭失敗,都是插入到隊列的尾節點,tail節點始終指向隊列中的最后一個元素。

每個節點中, 除了存儲了當前線程,前后節點的引用以外,還有一個waitStatus變量,用于描述節點當前的狀態。多線程并發執行時,隊列中會有多個節點存在,這個waitStatus其實代表對應線程的狀態:有的線程可能獲取鎖因為某些原因放棄競爭;有的線程在等待滿足條件,滿足之后才能執行等等。一共有4中狀態:

  1. CANCELLED 取消狀態
  2. SIGNAL 等待觸發狀態
  3. CONDITION 等待條件狀態
  4. PROPAGATE 狀態需要向后傳播

等待隊列是FIFO先進先出,只有前一個節點的狀態為SIGNAL時,當前節點的線程才能被掛起。

實現原理

子類重寫tryAcquire和tryRelease方法通過CAS指令修改狀態變量state。

public final void acquire(int arg) {   
 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))    
    selfInterrupt();
}
線程獲取鎖過程

下列步驟中線程A和B進行競爭。

  1. 線程A執行CAS執行成功,state值被修改并返回true,線程A繼續執行。
  2. 線程A執行CAS指令失敗,說明線程B也在執行CAS指令且成功,這種情況下線程A會執行步驟3。
  3. 生成新Node節點node,并通過CAS指令插入到等待隊列的隊尾(同一時刻可能會有多個Node節點插入到等待隊列中),如果tail節點為空,則將head節點指向一個空節點(代表線程B),具體實現如下:
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  1. node插入到隊尾后,該線程不會立馬掛起,會進行自旋操作。因為在node的插入過程,線程B(即之前沒有阻塞的線程)可能已經執行完成,所以要判斷該node的前一個節點pred是否為head節點(代表線程B),如果pred == head,表明當前節點是隊列中第一個“有效的”節點,因此再次嘗試tryAcquire獲取鎖,
    1、如果成功獲取到鎖,表明線程B已經執行完成,線程A不需要掛起。
    2、如果獲取失敗,表示線程B還未完成,至少還未修改state值。進行步驟5。
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  1. 前面我們已經說過只有前一個節點pred的線程狀態為SIGNAL時,當前節點的線程才能被掛起。
    1、如果pred的waitStatus == 0,則通過CAS指令修改waitStatus為Node.SIGNAL。
    2、如果pred的waitStatus > 0,表明pred的線程狀態CANCELLED,需從隊列中刪除。
    3、如果pred的waitStatus為Node.SIGNAL,則通過LockSupport.park()方法把線程A掛起,并等待被喚醒,被喚醒后進入步驟6。
    具體實現如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  1. 線程每次被喚醒時,都要進行中斷檢測,如果發現當前線程被中斷,那么拋出InterruptedException并退出循環。從無限循環的代碼可以看出,并不是被喚醒的線程一定能獲得鎖,必須調用tryAccquire重新競爭,因為鎖是非公平的,有可能被新加入的線程獲得,從而導致剛被喚醒的線程再次被阻塞,這個細節充分體現了“非公平”的精髓。

線程釋放鎖過程:
  1. 如果頭結點head的waitStatus值為-1,則用CAS指令重置為0;
  2. 找到waitStatus值小于0的節點s,通過LockSupport.unpark(s.thread)喚醒線程。
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

總結

Doug Lea大神的思路跳躍的太快,把CAS指令玩的出神入化,以至于有些邏輯反反復復debug很多次才明白。

END。
我是占小狼。
在魔都艱苦奮斗,白天是上班族,晚上是知識服務工作者。
讀完我的文章有收獲,記得關注和點贊哦,如果非要打賞,我也是不會拒絕的啦!

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

推薦閱讀更多精彩內容