抽象同步隊列AQS——AbstractQueuedSynchronizer鎖詳解

AQS——鎖的底層支持

談到并發(fā),不得不談ReentrantLock;而談到ReentrantLock,不得不談AbstractQueuedSynchronizer(AQS)!

類如其名,抽象的隊列式的同步器,AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現(xiàn)都依賴于它,如常用的ReentrantLock/Semaphore/CountDownLatch...

并發(fā)包的底層就是使用AQS實現(xiàn)的,以下是AQS的類圖結構

image

框架

image

它維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程競爭資源被阻塞會進入此隊列)。這里volatile保證線程可見性。

state的訪問方式有三種:

getState()

setState()

compareAndSetState()

這三種都是原子操作,其中compareAndSetState的實現(xiàn)依賴于Unsafe的compareAndSwapInt()方法。代碼如下:

<pre style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; word-break: initial; overflow-wrap: initial; white-space: pre; overflow: auto; margin: 16px 0px 14px; padding: 14px 15px 12px; border-radius: 3px; border: none; display: block; line-height: 1.6; background: rgb(246, 246, 246); color: rgb(61, 70, 77); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}</pre>

自定義資源共享方式

AQS定義了兩種資源共享方式:Exclusive(獨占,只有一個線程能執(zhí)行,如ReentantLock)和Share(共享,多個線程可同時執(zhí)行,如Semaphore/CountDownLatch)。

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

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

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

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

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

tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放后允許喚醒后續(xù)等待節(jié)點返回true,否則返回false。

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

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

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

源碼實現(xiàn)

接下來我們開始開始講解AQS的源碼實現(xiàn)。依照acquire-release、acquireShared-releaseShared的次序來。

1. acquire(int)

acquire是一種以獨占方式獲取資源,如果獲取到資源,線程直接返回,否則進入等待隊列,直到獲取到資源為止,且整個過程忽略中斷的影響。該方法是獨占模式下線程獲取共享資源的頂層入口。

獲取到資源后,線程就可以去執(zhí)行其臨界區(qū)代碼了。下面是acquire()的源碼
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}</pre>

通過注釋我們知道,acquire方法是一種互斥模式,且忽略中斷。該方法至少執(zhí)行一次tryAcquire(int)方法,如果tryAcquire(int)方法返回true,則acquire直接返回,否則當前線程需要進入隊列進行排隊。函數(shù)流程如下

1、tryAcquire():嘗試直接獲取資源,如果成功則直接返回;

2、addWaiter():將該線程加入等待隊列的尾部,并標記為獨占模式;

3、acquireQueued():使線程在等待隊列中獲取資源,一直獲取到資源后才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false。

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

現(xiàn)在開始對這四個方法進行源碼分析

1.1 tryAcquire(int)

protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}</pre>

tryAcquire嘗試以獨占的方式獲取資源,如果獲取成功,則直接返回true,否則直接返回false。該方法可以用于實現(xiàn)Lock中的tryLock()方法。該方法的默認實現(xiàn)是拋出UnsupportedOperationException異常,

什么?直接throw異常?說好的功能呢?好吧,還記得概述里講的AQS只是一個框架,具體資源的獲取/釋放方式交由自定義同步器去實現(xiàn)嗎?就是這里了!!!AQS這里只定義了一個接口,具體資源的獲取交由

自定義同步器去實現(xiàn)了(通過state的get/set/CAS)!!!至于能不能重入,能不能加塞,那就看具體的自定義同步器怎么去設計了!!!當然,自定義同步器在進行資源訪問時要考慮線程安全的影響。

這里之所以沒有定義成abstract,是因為獨占模式下只用實現(xiàn)tryAcquire-tryRelease,而共享模式下只用實現(xiàn)tryAcquireShared-tryReleaseShared。如果都定義成abstract,那么每個模式也要去實現(xiàn)另一模式下的接口。

說到底,Doug Lea還是站在咱們開發(fā)者的角度,盡量減少不必要的工作量。

1.2 addWaiter(Node)

** * Creates and enqueues node for current thread and given mode.

    • @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
  • @return the new node
    */
    private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode); //——以給定模式構造節(jié)點。mode有兩種:EXCLUSIVE(獨占)和SHARED(共享)
    // 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); //——上一步失敗則通過enq入隊
    return node;
    }</pre>

不用再說了,直接看注釋吧。這里我們說下Node。Node結點是對每一個訪問同步代碼的線程的封裝,其包含了需要同步的線程本身以及線程的狀態(tài),如是否被阻塞,是否等待喚醒,是否已經(jīng)被取消等。變量waitStatus則表示當前被封裝成Node結點的等待狀態(tài),共有4種取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。

  • CANCELLED:值為1,在同步隊列中等待的線程等待超時或被中斷,需要從同步隊列中取消該Node的結點,其結點的waitStatus為CANCELLED,即結束狀態(tài),進入該狀態(tài)后的結點將不會再變化。
  • SIGNAL:值為-1,被標識為該等待喚醒狀態(tài)的后繼結點,當其前繼結點的線程釋放了同步鎖或被取消,將會通知該后繼結點的線程執(zhí)行。說白了,就是處于喚醒狀態(tài),只要前繼結點釋放鎖,就會通知標識為SIGNAL狀態(tài)的后繼結點的線程執(zhí)行。
  • CONDITION:值為-2,與Condition相關,該標識的結點處于等待隊列中,結點的線程等待在Condition上,當其他線程調(diào)用了Condition的signal()方法后,CONDITION狀態(tài)的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。
  • PROPAGATE:值為-3,與共享模式相關,在共享模式中,該狀態(tài)標識結點的線程處于可運行狀態(tài)。
  • 0狀態(tài):值為0,代表初始化狀態(tài)。

AQS在判斷狀態(tài)時,通過用waitStatus>0表示取消狀態(tài),而waitStatus<0表示有效狀態(tài)。

1.2.1 enq(node)

/**

  • Inserts node into queue, initializing if necessary. See picture above.
  • @param node the node to insert
  • @return node's predecessor
    */
    private Node enq(final Node node) {
    for (;;) { //——CAS自旋,直到成功加入隊尾
    Node t = tail;
    if (t == null) { // Must initialize //——隊列為空,創(chuàng)建一個空的標示節(jié)點作為head節(jié)點,并將tail也指向它
    if (compareAndSetHead(new Node()))
    tail = head;
    } else { //——正常流程放入隊尾
    node.prev = t;
    if (compareAndSetTail(t, node)) {
    t.next = node;
    return t;
    }
    }
    }
    }</pre>

enq(node)用于將當前節(jié)點插入到等待隊列,如果隊列為空,則初始化當前隊列。整個過程以CAS自旋的方式進行,直到成功加入隊尾為止。

1.3 acquireQueued(Node, int)

OK,通過tryAcquire()和addWaiter(),該線程獲取資源失敗,已經(jīng)被放入等待隊列尾部了。聰明的你立刻應該能想到該線程下一部該干什么了吧:進入等待狀態(tài)休息,直到其他線程徹底釋放資源后喚醒自己,自己再拿到資源,然后就可以去干自己想干的事了。沒錯,就是這樣!是不是跟醫(yī)院排隊拿號有點相似~~acquireQueued()就是干這件事:在等待隊列中排隊拿號(中間沒其它事干可以休息),直到拿到號后再返回。這個函數(shù)非常關鍵,還是上源碼吧:

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//——標記是否成功拿到資源,默認是false
try {
boolean interrupted = false;//——標記等待過程是否被中斷過
for (;;) { //——又是一個自旋!
final Node p = node.predecessor(); //——拿到前驅
if (p == head && tryAcquire(arg)) { //——如果前驅是head,即該節(jié)點已成為老二,那么便有資源去嘗試獲取資源(可能是老大釋放完資源后喚醒自己的,當然也可能被interrupt了)
setHead(node); //——拿到資源后,將head指向該節(jié)點。所以head所指向的標桿節(jié)點,就是當前獲取到資源的那個節(jié)點或null
p.next = null; // help GC //setHead中的node.prev以置為null,此處再將head.next置為null,就是為了方便GC回收以前的head節(jié)點。也就意味著之前拿完資源的節(jié)點出隊了!
failed = false;
return interrupted; //——返回等待過程中是否被中斷過
}
//如果自己可以休息了,就進入waiting狀態(tài),直到被unpark()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; //如果等待過程被中斷過了,哪怕只有那么一次,就將interrupted標記為true
}
} finally {
if (failed)
cancelAcquire(node);
}
}</pre>

到這里了,我們先不急著總結acquireQueued()的函數(shù)流程,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具體干些什么。

1.3.1 shouldParkAfterFailedAcquire(Node, Node)

此方法主要用于檢查狀態(tài),看看自己是否真的可以去休息了,進入waiting狀態(tài)

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //——拿到前驅狀態(tài)
if (ws == Node.SIGNAL) //——如果已經(jīng)告訴前驅拿完號后通知自己一下,那就可以安心休息了
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
/
return true;
if (ws > 0) { //——如果前驅放棄了,那就一直往前找,直到找到一個最近正常等待的狀態(tài),并排在它的后邊,注意哪些放棄的節(jié)點,由于被自己加塞到他們前邊,他們相當于形成了一個無引用鏈,稍后GC回收。
/

* 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); //——如果前驅正常,那就把前驅的狀態(tài)設置為SIGNAL,告訴它拿完號后通知自己一下,有可能失敗,人家說不定剛釋放完呢!
}
return false;
}</pre>

整個流程中,如果前驅結點的狀態(tài)不是SIGNAL,那么自己就不能安心去休息,需要去找個安心的休息點,同時可以再嘗試下看有沒有機會輪到自己拿號。

1.3.2 parkAndCheckInterrupt()

如果線程找好安全休息點后,那就可以安心去休息了。此方法就是讓線程去休息,真正進入等待狀態(tài)。

private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}</pre>

park()會讓當前線程進入waiting狀態(tài)。在此狀態(tài)下,有兩種途徑可以喚醒該線程:1)被unpark();2)被interrupt()。

1.3.3 小結

OK,看了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),現(xiàn)在讓我們再回到acquireQueued(),總結下該函數(shù)的具體流程:

1、節(jié)點進入隊尾后,檢查狀態(tài),找到安全休息點

2、調(diào)用park()進入waiting狀態(tài),等待unpark()或interrupt()喚醒自己

3、被喚醒后,看自己是不是有資格能拿到號。如果能拿到,head指向當前節(jié)點,并返回從入隊到拿到號的整個過程中是否被中斷過;如果沒用拿到,繼續(xù)流程1

1.4 小結

OKOK,acquireQueued()分析完之后,我們接下來再回到acquire()!再貼上它的源碼吧:

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

再來總結下它的流程吧:

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

2、沒成功,則執(zhí)行addWaiter()將線程加入等待隊列的尾部并標記為獨占模式;

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

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

由于此函數(shù)是重中之重,我再用流程圖總結一下:

image

至此,acquire()的流程終于算是告一段落了。這也就是ReentrantLock.lock()的流程,不信你去看其lock()源碼吧,整個函數(shù)就是一條acquire(1)!!!

2.release(int)

上一小節(jié)已經(jīng)把acquire()說完了,這一小節(jié)就來講講它的反操作release()吧。此方法是獨占模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,

如果徹底釋放了(即state=0),它會喚醒等待隊列里的其他線程來獲取資源。這也正是unlock()的語義,當然不僅僅只限于unlock()。下面是release()的源碼:

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

邏輯并不復雜。它調(diào)用tryRelease()來釋放資源。有一點需要注意的是,它是根據(jù)tryRelease()的返回值來判斷該線程是否已經(jīng)完成釋放掉資源了!所以自定義同步器在設計tryRelease()的時候要明確這一點!!

2.1 tryRelease(int)

此方法嘗試去釋放指定量的資源。下面是tryRelease()的源碼:

protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}</pre>

跟tryAcquire()一樣,這個方法是需要獨占模式的自定義同步器去實現(xiàn)的。正常來說,tryRelease()都會成功的,因為這是獨占模式,該線程來釋放資源,那么它肯定已經(jīng)拿到獨占資源了,直接減掉相應量的資源即可(state-=arg),也不需要考慮線程安全的問題。但要注意它的返回值,上面已經(jīng)提到了,release()是根據(jù)tryRelease()的返回值來判斷該線程是否已經(jīng)完成釋放掉資源了!所以自義定同步器在實現(xiàn)時,如果已經(jīng)徹底釋放資源(state=0),要返回true,否則返回false。

2.2 unparkSuccessor(Node)

此方法用于喚醒等待隊列中下一個線程。下面是源碼:

int ws = node.waitStatus; //——這里node一般為當前線程所在的節(jié)點
if (ws < 0) //——置0當前線程所在的節(jié)點狀態(tài),允許失敗
    compareAndSetWaitStatus(node, ws, 0);
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) //——這里看到<=0的結點,還是有效的結點
            s = t;
}
if (s != null)
    LockSupport.unpark(s.thread); //——喚醒

這個函數(shù)并不復雜。一句話概括:用unpark()喚醒等待隊列中最前邊的那個未放棄線程,這里我們也用s來表示吧。此時,再和acquireQueued()聯(lián)系起來,s被喚醒后,進入if (p == head && tryAcquire(arg))的判斷(即使p!=head也沒關系,它會再進入shouldParkAfterFailedAcquire()尋找一個安全點。這里既然s已經(jīng)是等待隊列中最前邊的那個未放棄線程了,那么通過shouldParkAfterFailedAcquire()的調(diào)整,s也必然會跑到head的next結點,下一次自旋p==head就成立啦),然后s把自己設置成head標桿結點,表示自己已經(jīng)獲取到資源了,acquire()也返回了!!And then, DO what you WANT!

2.3 小結

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

3.acquireShared(int)

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

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

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

tryAcquireShared()嘗試獲取資源,成功則直接返回;
失敗則通過doAcquireShared()進入等待隊列,直到獲取到資源為止才返回。

3.1.doAcquireShared(int)

此方法用于將當前線程加入等待隊列尾部休息,直到其他線程釋放資源喚醒自己,自己成功拿到相應量的資源后才返回。下面是doAcquireShared()的源碼:

private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED); //——加入隊列尾部
boolean failed = true; //——是否成功標志
try {
boolean interrupted = false; //——等待過程中是否被中斷的標志
for (;;) {
final Node p = node.predecessor(); //前驅
if (p == head) { //如果到head的下一個,因為head是拿到資源的線程,此時node被喚醒,很可能是head用完資源來喚醒自己
int r = tryAcquireShared(arg); //嘗試獲取資源
if (r >= 0) { //成功
setHeadAndPropagate(node, r); //將head指向自己,還有剩余資源可以再喚醒之后的線程
p.next = null; // help GC
if (interrupted) //如果等待過程中被打斷過,直接將中斷補上
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && //判斷狀態(tài),尋找安全點,進入waiting狀態(tài),等被unpark()或interrupt()
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}</pre>

有木有覺得跟acquireQueued()很相似?對,其實流程并沒有太大區(qū)別。只不過這里將補中斷的selfInterrupt()放到doAcquireShared()里了,而獨占模式是放到acquireQueued()之外,其實都一樣,不知道Doug Lea是怎么想的。

跟獨占模式比,還有一點需要注意的是,這里只有線程是head.next時(“老二”),才會去嘗試獲取資源,有剩余的話還會喚醒之后的隊友。那么問題就來了,假如老大用完后釋放了5個資源,而老二需要6個,老三需要1個,老四需要2個。老大先喚醒老二,老二一看資源不夠,他是把資源讓給老三呢,還是不讓?答案是否定的!老二會繼續(xù)park()等待其他線程釋放資源,也更不會去喚醒老三和老四了。獨占模式,同一時刻只有一個線程去執(zhí)行,這樣做未嘗不可;但共享模式下,多個線程是可以同時執(zhí)行的,現(xiàn)在因為老二的資源需求量大,而把后面量小的老三和老四也都卡住了。當然,這并不是問題,只是AQS保證嚴格按照入隊順序喚醒罷了(保證公平,但降低了并發(fā))。

3.1.1 setHeadAndPropagate(Node, int)

private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node); //head指向自己
//如果還有剩余資源,繼續(xù)喚醒下一個鄰居線程
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}</pre>

此方法在setHead()的基礎上多了一步,就是自己蘇醒的同時,如果條件符合(比如還有剩余資源),還會去喚醒后繼結點,畢竟是共享模式!

3.2 小結

OK,至此,acquireShared()也要告一段落了。讓我們再梳理一下它的流程:

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

其實跟acquire()的流程大同小異,只不過多了個自己拿到資源后,還會去喚醒后繼隊友的操作(這才是共享嘛)

4. releaseShared(int)

上一小節(jié)已經(jīng)把acquireShared()說完了,這一小節(jié)就來講講它的反操作releaseShared()吧。此方法是共享模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果成功釋放且允許喚醒等待線程,它會喚醒等待隊列里的其他線程來獲取資源。下面是releaseShared()的源碼:

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

此方法的流程也比較簡單,一句話:釋放掉資源后,喚醒后繼。跟獨占模式下的release()相似,但有一點稍微需要注意:獨占模式下的tryRelease()在完全釋放掉資源(state=0)后,才會返回true去喚醒其他線程,這主要是基于獨占下可重入的考量;而共享模式下的releaseShared()則沒有這種要求,共享模式實質就是控制一定量的線程并發(fā)執(zhí)行,那么擁有資源的線程在釋放掉部分資源時就可以喚醒后繼等待結點。例如,資源總量是13,A(5)和B(7)分別獲取到資源并發(fā)運行,C(4)來時只剩1個資源就需要等待。A在運行過程中釋放掉2個資源量,然后tryReleaseShared(2)返回true喚醒C,C一看只有3個仍不夠繼續(xù)等待;隨后B又釋放2個,tryReleaseShared(2)返回true喚醒C,C一看有5個夠自己用了,然后C就可以跟A和B一起運行。而ReentrantReadWriteLock讀鎖的tryReleaseShared()只有在完全釋放掉資源(state=0)才返回true,所以自定義同步器可以根據(jù)需要決定tryReleaseShared()的返回值。

4.1 doReleaseShared()

此方法主要用于喚醒后繼。下面是它的源碼:

for (;;) {
    Node h = head;
    if (h != null && h != tail) {
        int ws = h.waitStatus;
        if (ws == Node.SIGNAL) {
            if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                continue;// loop to recheck cases
            unparkSuccessor(h); //喚醒后繼
        }
        else if (ws == 0 &&
                 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
            continue; // loop on failed CAS
    }
    if (h == head) //head發(fā)生變化
        break;
}

5 小結

本節(jié)我們詳解了獨占和共享兩種模式下獲取-釋放資源(acquire-release、acquireShared-releaseShared)的源碼,相信大家都有一定認識了。值得注意的是,acquire()和acquireShared()兩種方法下,線程在等待隊列中都是忽略中斷的。AQS也支持響應中斷的,acquireInterruptibly()/acquireSharedInterruptibly()即是,這里相應的源碼跟acquire()和acquireShared()差不多,這里就不再詳解了。

四、簡單應用

4.1 Mutex(互斥鎖)

Mutex是一個不可重入的互斥鎖實現(xiàn)。鎖資源(AQS里的state)只有兩種狀態(tài):0表示未鎖定,1表示鎖定。下邊是Mutex的核心源碼:

class Mutex implements Lock, java.io.Serializable {
    // 自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 判斷是否鎖定狀態(tài)
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 嘗試獲取資源,立即返回。成功則返回true,否則false。
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // 這里限定只能為1個量
            if (compareAndSetState(0, 1)) {//state為0才設置為1,不可重入!
                setExclusiveOwnerThread(Thread.currentThread());//設置為當前線程獨占資源
                return true;
            }
            return false;
        }

        // 嘗試釋放資源,立即返回。成功則為true,否則false。
        protected boolean tryRelease(int releases) {
            assert releases == 1; // 限定為1個量
            if (getState() == 0)//既然來釋放,那肯定就是已占有狀態(tài)了。只是為了保險,多層判斷!
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);//釋放資源,放棄占有狀態(tài)
            return true;
        }
    }

    // 真正同步類的實現(xiàn)都依賴繼承于AQS的自定義同步器!
    private final Sync sync = new Sync();

    //lock<-->acquire。兩者語義一樣:獲取資源,即便等待,直到成功才返回。
    public void lock() {
        sync.acquire(1);
    }

    //tryLock<-->tryAcquire。兩者語義一樣:嘗試獲取資源,要求立即返回。成功則為true,失敗則為false。
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    //unlock<-->release。兩者語文一樣:釋放資源。
    public void unlock() {
        sync.release(1);
    }

    //鎖是否占有狀態(tài)
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

同步類在實現(xiàn)時一般都將自定義同步器(sync)定義為內(nèi)部類,供自己使用;而同步類自己(Mutex)則實現(xiàn)某個接口,對外服務。當然,接口的實現(xiàn)要直接依賴sync,它們在語義上也存在某種對應關系!!而sync只用實現(xiàn)資源state的獲取-釋放方式tryAcquire-tryRelelase,至于線程的排隊、等待、喚醒等,上層的AQS都已經(jīng)實現(xiàn)好了,我們不用關心。

除了Mutex,ReentrantLock/CountDownLatch/Semphore這些同步類的實現(xiàn)方式都差不多,不同的地方就在獲取-釋放資源的方式tryAcquire-tryRelelase。掌握了這點,AQS的核心便被攻破了!

OK,至此,整個AQS的講解也要落下帷幕了。

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

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