前言
在前面兩篇系列文章中,已經講解了獨佔鎖的獲取和釋放過程,而共享鎖的獲取與釋放過程也很類似,如果你前面獨佔鎖的內容都看懂了,那麼共享鎖你也就觸類旁通了。
共享鎖與獨佔鎖的區別
共享鎖與獨佔鎖最大的區別在於,共享鎖的函數名裏面都帶有一個Shared
(抖個機靈,當然不是這個)。
- 獨佔鎖是線程獨佔的,同一時刻只有一個線程能擁有獨佔鎖,AQS裏將這個線程放置到
exclusiveOwnerThread
成員上去。 - 共享鎖是線程共享的,同一時刻能有多個線程擁有共享鎖,但AQS裏並沒有用來存儲獲得共享鎖的多個線程的成員。
- 如果一個線程剛獲取了共享鎖,那麼在其之後等待的線程也很有可能能夠獲取到鎖。但獨佔鎖不會這樣做,因爲鎖是獨佔的。
- 當然,如果一個線程剛釋放了鎖,不管是獨佔鎖還是共享鎖,都需要喚醒在後面等待的線程。
讓我們把共享鎖與獨佔鎖的函數名都列出來看一下:
獨佔鎖 | 共享鎖 |
---|---|
tryAcquire(int arg) |
tryAcquireShared(int arg) |
tryAcquireNanos(int arg, long nanosTimeout) |
tryAcquireSharedNanos(int arg, long nanosTimeout) |
acquire(int arg) |
acquireShared(int arg) |
acquireQueued(final Node node, int arg) |
doAcquireShared(int arg) |
acquireInterruptibly(int arg) |
acquireSharedInterruptibly(int arg) |
doAcquireInterruptibly(int arg) |
doAcquireSharedInterruptibly(int arg) |
doAcquireNanos(int arg, long nanosTimeout) |
doAcquireSharedNanos(int arg, long nanosTimeout) |
release(int arg) |
releaseShared(int arg) |
tryRelease(int arg) |
tryReleaseShared(int arg) |
- | doReleaseShared() |
從上表可以看到,共享鎖的函數是和獨佔鎖是一一對應的,而且大部分只是函數名加了個Shared
,從邏輯上看也是很相近的。
而doReleaseShared
沒有對應到獨佔鎖的方法是因爲它的邏輯是包含了unparkSuccessor
,是建立在unparkSuccessor
之上的,你可以簡單地認爲,doReleaseShared
對應到獨佔鎖的方法是unparkSuccessor
。最主要的是,它們的使用時機不同:
- 在獨佔鎖中,釋放鎖時,會調用
unparkSuccessor
。 - 在共享鎖中,獲得鎖和釋放鎖時,都會調用到
doReleaseShared
。不過獲得共享鎖時,是在一定條件下調用doReleaseShared
。
觀察Semaphore的內部類
爲了看到AQS的子類實現部分,我們從Semaphore看起。
abstract static class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) {
setState(permits);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
static final class NonfairSync extends Sync {
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
static final class FairSync extends Sync {
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
- 首先看到
Sync
的構造器,看來參數permits
是代表共享鎖的數量。 - 觀察
tryAcquireShared
的公平和非公平鎖的邏輯,發現區別只是 公平鎖裏面每次循環都會判斷hasQueuedPredecessors()
的返回值。
這裏先給大家講一下tryAcquireShared
:
參數acquires
代表這次想要獲得的共享鎖的數量是多少。
返回值則有三種情況:
- 如果返回值大於0,說明獲取共享鎖成功,並且後續獲取也可能獲取成功。
- 如果返回值等於0,說明獲取共享鎖成功,但後續獲取可能不會成功。
- 如果返回值小於0,說明獲取共享鎖失敗。
直接看公平版本的tryAcquireShared
,上面返回的地方:
hasQueuedPredecessors()
如果返回了true,說明有線程排在了當前線程之前,現在公平版本又不能插隊,所以結束返回-1,代表獲取失敗。- 如果
remaining < 0
成立,說明想要獲取的共享鎖數量已經超過了當前已有的數量,那麼直接返回一個負數remaining
,代表獲取失敗。 - 如果
remaining < 0
不成立,說明想要獲取的共享鎖數量沒有超過了當前已有的數量(等於0代表將會獲取剩餘所有的共享鎖)。且接下來如果compareAndSetState(available, remaining)
成功,那麼返回一個>=0
的數remaining
,代表獲取成功。
接下來我們談談共享鎖的tryAcquireShared
和獨佔鎖的tryAcquire
的不同之處:
tryAcquire
的返回值是boolean型,它只代表兩種狀態(獲取成功或失敗)。而tryAcquireShared
的返回值是int型,如上有三種情況。tryAcquireShared
使用了自旋(死循環),但tryAcquire
沒有自旋。這將導致tryAcquire
最多執行一次CAS操作修改同步器狀態,但tryAcquireShared
可能有多次。tryAcquireShared
具體地講,只要remaining
是>=0
的(remaining < 0
不成立),就一定會去嘗試CAS設置同步器的狀態。使用自旋的原因想必是,鎖是共享的,既然還可能獲取到(remaining
是>=0
的),就一定要去嘗試。
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
最後再看tryReleaseShared
的實現,也用到了自旋操作,因爲完全有可能多個線程同時釋放共享鎖,同時調用tryReleaseShared
,所以需要用自旋保證 共享鎖的釋放最終能體現到同步器的狀態上去。另外,除非int型溢出,那麼此函數只可能返回true。
共享鎖的獲取
上面講完了Semaphore的內部類,接下來我們就可以盡情地在AQS的源碼裏暢遊了。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
acquireShared
對應到獨佔鎖的方法是acquire
:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
咋一看感覺差別有點大,其實我們被迷惑了,後面我們會發現,之所以acquireShared
裏沒有顯式調用addWaiter
和selfInterrupt
,是因爲這兩件事都被放到了doAcquireShared(arg)
的邏輯裏面了。
接下來看看doAcquireShared
方法的邏輯,它對應到獨佔鎖是acquireQueued
,除了上面提到的兩件事,它們其實差別很少:
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時,才嘗試獲得共享鎖
int r = tryAcquireShared(arg);
if (r >= 0) { //獲取共享鎖成功時,才進行善後操作
setHeadAndPropagate(node, r); //獨佔鎖這裏調用的是setHead
p.next = null;
if (interrupted)
selfInterrupt(); //這件事也放到裏面來了
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
而acquireQueued
在獲得獨佔鎖成功時,執行的是:
if (p == head && tryAcquire(arg)) { // tryAcquire返回true,代表獲取獨佔鎖成功
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
所以對比發現,共享鎖的doAcquireShared
有兩處不同:
- 創建的節點不同。共享鎖使用
addWaiter(Node.SHARED)
,所以會創建出想要獲取共享鎖的節點。而獨佔鎖使用addWaiter(Node.EXCLUSIVE)
。 - 獲取鎖成功後的善後操作不同。共享鎖使用
setHeadAndPropagate(node, r)
,因爲剛獲取共享鎖成功後,後面的線程也有可能成功獲取,所以需要在一定條件喚醒head後繼。而獨佔鎖使用setHead(node)
。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
setHead
函數只是將剛成爲將成爲head的節點變成一個dummy node。而setHeadAndPropagate
裏也會調用setHead
函數。但是它在一定條件下還可能會調用doReleaseShared
,看來這就是單詞Propagate
的由來了,也就是我們一直說的“如果一個線程剛獲取了共享鎖,那麼在其之後等待的線程也很有可能能夠獲取到鎖”。
doReleaseShared
留到之後講解,因爲共享鎖的釋放也會用到它。
關於setHeadAndPropagate的詳解請看這篇setHeadAndPropagate源碼分析,主要有兩張圖幫助大家理解setHeadAndPropagate
裏的這個超長的if
判斷。
共享鎖的釋放
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
releaseShared
對應到獨佔鎖的方法是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;
}
可見獨佔鎖的邏輯比較簡單,只是在head狀態不爲0時,就喚醒head後繼。
而共享鎖的邏輯則直接調用了doReleaseShared
,但在獲取共享鎖成功時,也可能會調用到doReleaseShared
。也就是說,獲取共享鎖的線程(分爲:已經獲取到的線程 即執行setHeadAndPropagate
中、等待獲取中的線程 即阻塞在shouldParkAfterFailedAcquire
裏)和釋放共享鎖的線程 可能在同時執行這個doReleaseShared
。
private void 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) // loop if head changed
break;
}
}
我們來仔細分析下這個函數的邏輯:
- 邏輯是一個死循環,每次循環中重新讀取一次head,然後保存在局部變量
h
中,再配合if(h == head) break;
,這樣,循環檢測到head沒有變化時就會退出循環。注意,head變化一定是因爲:acquire thread被喚醒,之後它成功獲取鎖,然後setHead設置了新head。而且注意,只有通過if(h == head) break;
即head不變才能退出循環,不然會執行多次循環。 if (h != null && h != tail)
判斷隊列是否至少有兩個node,如果隊列從來沒有初始化過(head爲null),或者head就是tail,那麼中間邏輯直接不走,直接判斷head是否變化了。- 如果隊列中有兩個或以上個node,那麼檢查局部變量
h
的狀態:- 如果狀態爲SIGNAL,說明h的後繼是需要被通知的。通過對CAS操作結果取反,將
compareAndSetWaitStatus(h, Node.SIGNAL, 0)
和unparkSuccessor(h)
綁定在了一起。說明了只要head成功得從SIGNAL修改爲0,那麼head的後繼的代表線程肯定會被喚醒了。 - 如果狀態爲0,說明h的後繼所代表的線程已經被喚醒或即將被喚醒,並且這個中間狀態即將消失,要麼由於acquire thread獲取鎖失敗再次設置head爲SIGNAL並再次阻塞,要麼由於acquire thread獲取鎖成功而將自己(head後繼)設置爲新head並且只要head後繼不是隊尾,那麼新head肯定爲SIGNAL。所以設置這種中間狀態的head的status爲PROPAGATE,讓其status又變成負數,這樣可能被被喚醒線程檢測到。
- 如果狀態爲PROPAGATE,直接判斷head是否變化。
- 如果狀態爲SIGNAL,說明h的後繼是需要被通知的。通過對CAS操作結果取反,將
- 兩個continue保證了進入那兩個分支後,只有當CAS操作成功後,纔可能去執行
if(h == head) break;
,纔可能退出循環。 if(h == head) break;
保證了,只要在某個循環的過程中有線程剛獲取了鎖且設置了新head,就會再次循環。目的當然是爲了再次執行unparkSuccessor(h)
,即喚醒隊列中第一個等待的線程。
head狀態爲0的情況
- 如果等待隊列中只有一個dummy node(它的狀態爲0),那麼head也是tail,且head的狀態爲0。
- 等待隊列中當前只有一個dummy node(它的狀態爲0),acquire thread獲取鎖失敗了(無論獨佔還是共享),將當前線程包裝成node放到隊列中,此時隊列中有兩個node,但當前線程還沒來得及執行一次
shouldParkAfterFailedAcquire
。 - 此時隊列中有多個node,有線程剛釋放了鎖,剛執行了
unparkSuccessor
裏的if (ws < 0) compareAndSetWaitStatus(node, ws, 0);
把head的狀態設置爲了0,然後喚醒head後繼線程,head後繼線程獲取鎖成功,直到head後繼線程將自己設置爲AQS的新head的這段時間裏,head的狀態爲0。- 具體地講,如果是共享鎖的話,一定是在調用
unparkSuccessor
之前就把head的狀態變成0了,因爲if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
。 - 上面這種情況還可以繼續延伸,在“喚醒head後繼線程”後,head後繼線程喚醒後第一次循環獲取鎖失敗(你可能會疑問,上面的場景明明是剛有人釋放了鎖,爲什麼這裏會失敗,因爲多線程環境下有可能被別的不公平獲取方式插隊了),調用
shouldParkAfterFailedAcquire
又將head設置回SIGNAL了,然後第二次循環開始之前(假設head後繼線程此時分出去時間片),又有一個釋放鎖的線程在執行doReleaseShared
裏面的compareAndSetWaitStatus(h, Node.SIGNAL, 0)
成功並且還unpark了處於喚醒狀態的head後繼線程,然後第二次循環開始(假設head後繼線程此時得到時間片),獲取鎖成功。- 注意,如果unpark一個已經喚醒的線程,它的副作用是下一次park這個線程,線程不會阻塞。下下次park線程,纔會阻塞。
- 具體地講,如果是共享鎖的話,一定是在調用
總結:
- head狀態爲0的情況,屬於一種中間狀態。
- 這種中間狀態將變化爲,head狀態爲SIGNAL,不管acquire thread接下來是獲取鎖成功還是失敗。不過獲取鎖成功這種情況,需要考慮head後繼(也就是包裝acquire thread的那個node)不是隊尾,如果是隊尾,那麼新head的狀態也是爲0的了。
同時執行doReleaseShared
這個函數的難點在於,很可能有多個線程同時在同時運行它。比如你創建了一個Semaphore(0)
,讓N個線程執行acquire()
,自然這多個線程都會阻塞在acquire()
這裏,然後你讓另一個線程執行release(N)
。
- 此時 釋放共享鎖的線程,肯定在執行doReleaseShared。
- 由於 上面這個線程的unparkSuccessor,head後繼的代表線程也會喚醒,進而執行doReleaseShared。
- 重複第二步,獲取共享鎖的線程 又會喚醒 新head後繼的代表線程。
觀察上面過程,有的線程 因爲CAS操作失敗,或head變化(主要是因爲這個),會一直退不出循環。進而,可能會有多個線程都在運行該函數。doReleaseShared源碼分析中的圖解舉例了一種循環繼續的例子,當然,循環繼續的情況有很多。
總結
- 共享鎖與獨佔鎖的最大不同,是共享鎖可以同時被多個線程持有,雖然AQS裏面沒有成功用來保存持有共享鎖的線程們。
- 由於共享鎖在獲取鎖和釋放鎖時,都需要喚醒head後繼,所以將其邏輯抽取成一個
doReleaseShared
的邏輯了。