java并發(fā)編程之AbstractQueuedSynchronizer

引言


AbstractQueuedSynchronizer挑围,隊列同步器,簡稱AQS恢氯,它是java并發(fā)用來構(gòu)建鎖或者其他同步組件的基礎(chǔ)框架。

一般使用AQS的主要方式是繼承鼓寺,子類通過實現(xiàn)它提供的抽象方法來管理同步狀態(tài)勋拟,主要管理的方式是通過tryAcquire和tryRelease類似的方法來操作狀態(tài),同時侄刽,AQS提供以下線程安全的方法來對狀態(tài)進行操作:

protected final int getState();
protected final void setState(int newState);
protected final boolean compareAndSetState(int expect, int update);

AQS本身是沒有實現(xiàn)任何同步接口的指黎,它僅僅只是定義了同步狀態(tài)的獲取和釋放的方法來供自定義的同步組件的使用朋凉。

注:AQS主要是怎么使用的呢州丹?
在java的同步組件中,AQS的子類一般是同步組件的靜態(tài)內(nèi)部類杂彭。

AQS是實現(xiàn)同步組件的關(guān)鍵墓毒,它倆的關(guān)系可以這樣描述:同步組件是面向使用者的,它定義了使用者與組件交互的接口亲怠,隱藏了具體的實現(xiàn)細節(jié)所计;而AQS面向的是同步組件的實現(xiàn)者,它簡化了具體的實現(xiàn)方式团秽,屏蔽了線程切換相關(guān)底層操作主胧,它們倆一起很好的對使用者和實現(xiàn)者所關(guān)注的領(lǐng)域做了一個隔離。

AQS實現(xiàn)分析


接下來將從實現(xiàn)的角度來具體分析AQS是如何來完成線程同步的习勤。

同步隊列分析

AQS的實現(xiàn)依賴內(nèi)部的同步隊列(FIFO雙向隊列)來完成同步狀態(tài)的管理踪栋,假如當(dāng)前線程獲取同步狀態(tài)失敗,AQS會將該線程以及等待狀態(tài)等信息構(gòu)造成一個Node图毕,并將其加入同步隊列夷都,同時阻塞當(dāng)前線程。當(dāng)同步狀態(tài)釋放時予颤,喚醒隊列的首節(jié)點囤官。

  • Node
    Node主要包含以下成員變量:
static final class Node {
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
}
- ```waitStatus```:節(jié)點狀態(tài)冬阳,主要有這幾種狀態(tài):
Node節(jié)點狀態(tài)
  1. ```CANCELLED```:當(dāng)前線程被取消;
  2. ```SIGNAL```:當(dāng)前節(jié)點的后繼節(jié)點需要運行党饮;
  3. ```CONDITION```:當(dāng)前節(jié)點在等待condition肝陪;
  4. ```PROPAGATE```:當(dāng)前場景下后續(xù)的acquireShared可以執(zhí)行;
  5. 0:當(dāng)前節(jié)點在sync隊列中等待獲取鎖刑顺。
  • prev:前驅(qū)節(jié)點见坑;
  • next:后繼節(jié)點;
  • thread:進入隊列的當(dāng)前線程捏检;
  • nextWaiter:存儲condition隊列中的后繼節(jié)點荞驴。

Node是sync隊列和condition隊列構(gòu)建的基礎(chǔ),AQS擁有三個成員變量:

AQS成員變量

對于鎖的獲取贯城,請求形成節(jié)點將其掛在隊列尾部熊楼,至于資源的轉(zhuǎn)移,是從頭到尾進行能犯,隊列的基本結(jié)構(gòu)就出來了:

AQS同步隊列結(jié)構(gòu)
  • 同步隊列插入/刪除節(jié)點

    • 節(jié)點插入
      AQS提供基于CAS的設(shè)置尾節(jié)點的方法:


      CAS設(shè)置尾節(jié)點

    需要傳遞當(dāng)前線程認為的尾節(jié)點和當(dāng)前節(jié)點鲫骗,設(shè)置成功后,當(dāng)前節(jié)點與尾節(jié)點建立關(guān)聯(lián)踩晶。

    同步隊列插入節(jié)點
    • 節(jié)點刪除
      同步隊列遵循FIFO执泰,首節(jié)點是獲取同步狀態(tài)成功的節(jié)點,首節(jié)點的線程在釋放同步狀態(tài)之后將會喚醒后繼節(jié)點渡蜻,后繼節(jié)點將會在獲取同步狀態(tài)成功的時候?qū)⒆约涸O(shè)置為首節(jié)點术吝。

      同步隊列刪除節(jié)點

注:設(shè)置首節(jié)點是由獲取同步狀態(tài)成功的線程來完成,因為每次只會有一個線程能夠成功的獲取到同步狀態(tài)茸苇,所以排苍,設(shè)置首節(jié)點并不需要CAS來保證。

AQS源碼解析

AQS提供以下接口以供實現(xiàn)自定義同步器:

  1. protected boolean tryAcquire(int arg)
    獨占式獲取同步狀態(tài)学密,該方法的實現(xiàn)需要先查詢當(dāng)前的同步狀態(tài)是否可以獲取淘衙,如果可以獲取再進行獲取腻暮;
  2. protected boolean tryRelease(int arg)
    釋放狀態(tài)彤守;
  3. protected int tryAcquireShared(int arg)
    共享式獲取同步狀態(tài);
  4. protected boolean tryReleaseShared(int arg)
    共享式釋放狀態(tài)哭靖;
  5. protected boolean isHeldExclusively()
    獨占模式下具垫,判斷同步狀態(tài)是否已經(jīng)被占用。

使用者可以根據(jù)實際情況使用這些接口自定義同步組件款青。

AQS提供兩種方式來操作同步狀態(tài)做修,獨占式與共享式,下面就針對性做一下源碼分析。

獨占式同步狀態(tài)獲取 - acquire實現(xiàn)

獨占式同步狀態(tài)獲取

具體執(zhí)行流程如下:

  1. 調(diào)用tryAcquire方法嘗試獲取同步狀態(tài)饰及;
  2. 如果獲取不到同步狀態(tài)蔗坯,將當(dāng)前線程構(gòu)造成節(jié)點Node并加入同步隊列;
  3. 再次嘗試獲取燎含,如果還是沒有獲取到那么將當(dāng)前線程從線程調(diào)度器上摘下宾濒,進入等待狀態(tài)。

下面我們具體來看一下節(jié)點的構(gòu)造以及加入同步隊列部分的代碼實現(xiàn)屏箍。

  • addWaiter實現(xiàn)
addWaiter實現(xiàn)
  1. 使用當(dāng)前thread構(gòu)造Node绘梦;
  2. 嘗試在隊尾插入節(jié)點,如果尾節(jié)點已經(jīng)存在赴魁,就做以下操作:
- 分配引用T指向尾節(jié)點卸奉;
- 將待插入節(jié)點的prev指針指向尾節(jié)點;
- 如果尾節(jié)點還為T颖御,將當(dāng)前尾節(jié)點設(shè)置為帶待插入節(jié)點榄棵;
- T的next指針指向待插入節(jié)點。
  1. 快速在隊尾插入節(jié)點潘拱,失敗則進入enq(Node node)方法疹鳄。
  • enq實現(xiàn)
enq實現(xiàn)

enq的邏輯可以確保Node可以有順序的添加到同步隊列中,具體的加入隊列的邏輯如下:

  1. 初始化同步隊列:如果尾節(jié)點為空芦岂,分配一個頭結(jié)點瘪弓,并將尾節(jié)點指向頭結(jié)點;
  2. 節(jié)點入隊禽最,通過CAS將節(jié)點設(shè)置為尾節(jié)點腺怯,以此在隊尾做節(jié)點插入。

可以看出弛随,整個enq方法通過“死循環(huán)”來保證節(jié)點的正確插入瓢喉。

進入同步隊列之后接下來就是同步狀態(tài)的獲取了宁赤,或者說是訪問控制acquireQueued舀透。對于同步隊列中的線程,在同一時刻只能由隊列首節(jié)點獲取同步狀態(tài)决左,其他的線程進入等待愕够,直到符合條件才能繼續(xù)進行。

  • acquireQueued實現(xiàn)
acquireQueued實現(xiàn)
  1. 獲取當(dāng)前節(jié)點的前驅(qū)節(jié)點佛猛;
  2. 如果當(dāng)前節(jié)點的前驅(qū)節(jié)點是頭節(jié)點惑芭,并且可以獲取同步狀態(tài),設(shè)置當(dāng)前節(jié)點為頭結(jié)點继找,該節(jié)點占有鎖遂跟;
  3. 不滿足條件的線程進入等待狀態(tài)。

在整個方法中,當(dāng)前線程一直都在“死循環(huán)”中嘗試獲取同步狀態(tài):

節(jié)點自旋獲取同步狀態(tài)

從代碼的邏輯也可以看出幻锁,其實在節(jié)點與節(jié)點之間在循環(huán)檢查的過程中是不會相互通信的凯亮,僅僅只是判斷自己當(dāng)前的前驅(qū)是不是頭結(jié)點,這樣設(shè)計使得節(jié)點的釋放符合FIFO哄尔,同時也避免了過早通知假消。

注:過早通知是指前驅(qū)節(jié)點不是頭結(jié)點的線程由于中斷被喚醒。

  • acquire實現(xiàn)總結(jié)

    1. 同步狀態(tài)維護:
      對同步狀態(tài)的操作是原子岭接、非阻塞的富拗,通過AQS提供的對狀態(tài)訪問的方法來對同步狀態(tài)進行操作,并且利用CAS來確保原子操作鸣戴;
    2. 狀態(tài)獲瓤谢Α:
      一旦線程成功的修改了同步狀態(tài),那么該線程會被設(shè)置為同步隊列的頭節(jié)點窄锅;
    3. 同步隊列維護:
      不符合獲取同步狀態(tài)的線程會進入等待狀態(tài)谅阿,直到符合條件被喚醒再開始執(zhí)行。

    整個執(zhí)行流程如下:


    acquire流程圖

當(dāng)前線程獲取同步狀態(tài)并執(zhí)行了相應(yīng)的邏輯之后酬滤,就需要釋放同步狀態(tài)签餐,讓后續(xù)節(jié)點可以獲取到同步狀態(tài),調(diào)用方法release(int arg)方法可以釋放同步狀態(tài)盯串。

獨占式同步狀態(tài)釋放 - release實現(xiàn)

release實現(xiàn)

  1. 嘗試釋放狀態(tài)氯檐,tryRelease保證將狀態(tài)重置回去,同樣采用CAS來保證操作的原子性;
  2. 釋放成功后萨螺,調(diào)用unparkSuccessor喚醒當(dāng)前節(jié)點的后繼節(jié)點線程鹊漠。
  • unparkSuccessor實現(xiàn)

unparkSuccessor實現(xiàn)

取出當(dāng)前節(jié)點的next節(jié)點,將該節(jié)點線程喚醒河泳,被喚醒的線程獲取同步狀態(tài)。這里主要通過LockSupportunpark方法喚醒線程年栓。

共享式同步狀態(tài)獲取
共享式獲取與獨占式獲取最主要的區(qū)別就是在同一時刻能否有多個線程可以同時獲取到同步狀態(tài)拆挥。這兩種不同的方式在獲取資源區(qū)別如下圖所示:

共享式與獨占式獲取資源
  1. 共享式訪問資源時,其他共享式訪問都是被允許的某抓;
  2. 獨占式訪問資源時纸兔,在同一時刻只能有一個訪問,其他的訪問都被阻塞否副。

AQS提供acquireShared方法來支持共享式獲取同步狀態(tài)汉矿。

  • acquireShared實現(xiàn)
acquireShared實現(xiàn)
  1. 調(diào)用tryAcquireShared(int arg)方法嘗試獲取同步狀態(tài):
    tryAcquireShared方法返回值 > 0時,表示能夠獲取到同步狀態(tài)备禀;
  2. 獲取失敗調(diào)用doAcquireShared(int arg)方法進入同步隊列洲拇。
  • doAcquireShared實現(xiàn)
doAcquireShared實現(xiàn)
  1. 獲取當(dāng)前節(jié)點的前驅(qū)節(jié)點奈揍;
  2. 如果當(dāng)前節(jié)點的前驅(qū)節(jié)點是頭結(jié)點,并且獲取到的共享同步狀態(tài) > 0赋续,設(shè)置當(dāng)前節(jié)點的為頭結(jié)點打月,獲取同步狀態(tài)成功;
  3. 不滿足條件的線程自旋等待蚕捉。

與獨占式獲取同步狀態(tài)一樣奏篙,共享式獲取也是需要釋放同步狀態(tài)的,AQS提供releaseShared(int arg)方法可以釋放同步狀態(tài)迫淹。

共享式同步狀態(tài)釋放 - releaseShared實現(xiàn)

releaseShared實現(xiàn)
  1. 調(diào)用tryReleaseShared方法釋放狀態(tài)秘通;
  2. 調(diào)用doReleaseShared方法喚醒后繼節(jié)點;

獨占式超時獲取 - doAcquireNanos
該方法提供了超時獲取同步狀態(tài)調(diào)用敛熬,假如在指定的時間段內(nèi)可以獲取到同步狀態(tài)返回true肺稀,否則返回false。它是acquireInterruptibly(int arg)的增強版应民。

  • acquireInterruptibly實現(xiàn)
    該方法提供了獲取同步狀態(tài)的能力话原,同樣,在無法獲取同步狀態(tài)時會進入同步隊列诲锹,這類似于acquire的功能繁仁,但是它和acquire還是區(qū)別的:acquireInterruptibly可以在外界對當(dāng)前線程進行中斷的時候可以提前獲取到同步狀態(tài)的操作,換個通俗易懂的解釋吧:類似于synchronized獲取鎖時归园,這時候外界對當(dāng)前線程中斷了黄虱,線程獲取鎖的這個操作能夠及時響應(yīng)中斷并且提前返回。

    acquireInterruptibly實現(xiàn)
    1. 判斷當(dāng)前線程是否被中斷庸诱,如果已經(jīng)被中斷捻浦,拋出InterruptedException異常并將中斷標(biāo)志位置為false;
    2. 獲取同步狀態(tài)桥爽,獲取成功并返回朱灿,獲取不成功調(diào)用doAcquireInterruptibly(int arg)排隊等待。
  • doAcquireInterruptibly實現(xiàn)

    doAcquireInterruptibly實現(xiàn)
    1. 構(gòu)造節(jié)點Node钠四,加入同步隊列盗扒;
    2. 假如當(dāng)前節(jié)點是首節(jié)點并且可以獲取到同步狀態(tài),將當(dāng)前節(jié)點設(shè)置為頭結(jié)點形导,其他節(jié)點自旋等待环疼;
    3. 節(jié)點每次被喚醒的時候,需要進行中斷檢測朵耕,假如當(dāng)前線程被中斷,拋出異常InterruptedException淋叶,退出循環(huán)阎曹。
  • doAcquireNanos實現(xiàn)
    該方法在支持中斷響應(yīng)的基礎(chǔ)上,增加了超時獲取的特性。針對超時獲取处嫌,主要在于計算出需要睡眠的時間間隔nanosTimeout栅贴,如果nanosTimeout > 0表示當(dāng)前線程還需要睡眠,反之返回false熏迹。

    doAcquireNanos實現(xiàn)

    1. nanosTimeout <= 0檐薯,表明當(dāng)前線程不需要睡眠,返回false注暗,不能獲取到同步狀態(tài)坛缕;
    2. 不滿足條件的線程加入同步隊列;
    3. 假如當(dāng)前節(jié)點是首節(jié)點捆昏,并且可以獲取到同步狀態(tài)赚楚,將當(dāng)前節(jié)點設(shè)置為頭結(jié)點并退出,返回true骗卜,表明在指定的時間內(nèi)可以獲取到同步狀態(tài)宠页;
    4. 不滿足條件3的線程,計算出當(dāng)前休眠時間寇仓,nanosTimeout = 原有nanosTimeout + deadline(睡眠之前記錄的時間)- now(System.nanoTime():當(dāng)前時間):
    • 如果nanosTimeout <= 0举户,返回超時未獲取到同步狀態(tài);
    • 如果nanosTimeout > 0 && nanosTimeout <= 1000L遍烦,線程快速自旋

注:為什么不直接進入超時等待呢敛摘?原因在于非常短的超時等待是無法做到十分精確的,如果這時候再進入超時等待會讓nanosTimeout的超時從整體上表現(xiàn)的不精確乳愉,所以兄淫,在超時非常短的情況下,AQS都會無條件進入快速自旋蔓姚;
- 如果nanosTimeout > 1000L捕虽,線程通過LockSupport.parkNanos進入超時等待。

整個流程可以總結(jié)如下圖所示:


doAcquireNanos流程圖

后記

在上述對AQS進行了實現(xiàn)層面的分析之后坡脐,我們就一個案例來加深對AQS的理解泄私。

案例:設(shè)計一個AQS同步器,該工具在同一時刻备闲,只能有兩個線程能夠訪問晌端,其他的線程阻塞。

設(shè)計分析:針對上述案例恬砂,我們可以這樣定義AQS咧纠,設(shè)定一個初始狀態(tài)為2,每一個線程獲取一次就-1泻骤,正確的狀態(tài)為:0漆羔,1梧奢,2,0表示新的線程獲取同步狀態(tài)時阻塞演痒。由于資源數(shù)量大與1亲轨,需要實現(xiàn)tryAcquireSharedtryReleaseShared方法。

代碼實現(xiàn):

public class LockInstance implements Lock {
    
    private final Sync sync = new Sync(2);

    private static final class Sync extends AbstractQueuedSynchronizer { 
       Sync(int state) {
            if (state <= 0) {
                throw new IllegalArgumentException("count must large than 0");
            }
            setState(state);
        }

        @Override
        public int tryAcquireShared(int arg) {
            for (;;) {
                System.out.println("try acquire....");
                int current = getState();
                int now = current - arg;
                if (now < 0 || compareAndSetState(current, now)) {
                    return now;
                }
            }
        }

        @Override
        public boolean tryReleaseShared(int arg) {
            for(;;) {
                System.out.println("try release....");
                int current = getState();
                int now = current + arg;
                if (compareAndSetState(current, now)) {
                    return true;
                }
            }
        }
    }

    @Override
    public void lock() {
        sync.acquireShared(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquireShared(1) >= 0;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.tryReleaseShared(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸟顺,一起剝皮案震驚了整個濱河市惦蚊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌讯嫂,老刑警劉巖蹦锋,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異端姚,居然都是意外死亡晕粪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門渐裸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來巫湘,“玉大人,你說我怎么就攤上這事昏鹃∩蟹眨” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵洞渤,是天一觀的道長阅嘶。 經(jīng)常有香客問我,道長载迄,這世上最難降的妖魔是什么讯柔? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮护昧,結(jié)果婚禮上魂迄,老公的妹妹穿的比我還像新娘。我一直安慰自己惋耙,他們只是感情好捣炬,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绽榛,像睡著了一般湿酸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上灭美,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天推溃,我揣著相機與錄音,去河邊找鬼冲粤。 笑死美莫,一個胖子當(dāng)著我的面吹牛页眯,可吹牛的內(nèi)容都是我干的梯捕。 我是一名探鬼主播厢呵,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼傀顾!你這毒婦竟也來了襟铭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤短曾,失蹤者是張志新(化名)和其女友劉穎寒砖,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嫉拐,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡哩都,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了婉徘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漠嵌。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖盖呼,靈堂內(nèi)的尸體忽然破棺而出儒鹿,到底是詐尸還是另有隱情,我是刑警寧澤几晤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布约炎,位于F島的核電站,受9級特大地震影響蟹瘾,放射性物質(zhì)發(fā)生泄漏圾浅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一憾朴、第九天 我趴在偏房一處隱蔽的房頂上張望狸捕。 院中可真熱鬧,春花似錦伊脓、人聲如沸府寒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽株搔。三九已至,卻和暖如春纯蛾,著一層夾襖步出監(jiān)牢的瞬間纤房,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工翻诉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留炮姨,地道東北人捌刮。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像舒岸,于是被迫代替她去往敵國和親绅作。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容