聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁
上一篇聊聊高并發(二十八)解析java.util.concurrent各個組件(十) 理解ReentrantReadWriteLock可重入讀-寫鎖?講了可重入讀寫鎖的基本情況和主要的方法,顯示了如何實現的鎖降級。但是下面幾個問題沒說清楚,這篇補充一下
1. 釋放鎖時的優先級問題,是讓寫鎖先獲得還是先讓讀鎖先獲得
?
2. 是否允許讀線程插隊
3. 是否允許寫線程插隊,因為讀寫鎖一般用在大量讀,少量寫的情況,如果寫線程沒有優先級,那么可能造成寫線程的饑餓
?
關于釋放鎖后是讓寫鎖先獲得還是讓讀鎖先獲得,這里有兩種情況
1. 釋放鎖后,請求獲取寫鎖的線程不在AQS隊列
2. 釋放鎖后,請求獲取寫鎖的線程已經AQS隊列
?
如果是第一種情況,那么非公平鎖的實現下,獲取寫鎖的線程直接嘗試競爭鎖也不用管AQS里面先來的線程。獲取讀鎖的線程只判斷是否已經有線程獲得寫鎖(既Head節點是獨占模式的節點),如果沒有,那么就不用管AQS里面先來的準備獲取讀鎖的線程。
?
?static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
 在公平鎖的情況下,獲取讀鎖和寫鎖的線程都判斷是否已經或先來的線程再等待了,如果有,就進入AQS隊列等待。
?
?static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
 對于第二種情況,如果準備獲取寫鎖的線程在AQS隊列里面等待,那么實際是遵循先來先服務的公平性的,因為AQS的隊列是FIFO的隊列。所以獲取鎖的線程的順序是跟它在AQS同步隊列里的位置有關系。
下面這張圖模擬了AQS隊列中等待的線程節點的情況
?
1. Head節點始終是當前獲得了鎖的線程
2. 非Head節點在競爭鎖失敗后,acquire方法會不斷地輪詢,于自旋不同的是,AQS輪詢過程中的線程是阻塞等待。
所以要理解AQS的release釋放動作并不是讓后續節點直接獲取鎖,而是喚醒后續節點unparkSuccessor()。真正獲取鎖的地方還是在acquire方法,被release喚醒的線程繼續輪詢狀態,如果它的前驅是head,并且tryAcquire獲取資源成功了,那么它就獲得鎖
?
?public final boolean release(int arg) {
??????? if (tryRelease(arg)) {
??????????? Node h = head;
??????????? if (h != null && h.waitStatus != 0)
??????????????? unparkSuccessor(h);
??????????? return true;
??????? }
??????? return false;
??? }
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);
}
}
 ?
3. 圖中Head之后有3個準備獲取讀鎖的線程,最后是1個準備獲取寫鎖的線程。
那么如果是AQS隊列中的節點獲取鎖
情況是第一個讀鎖節點先獲得鎖,它獲取鎖的時候就會嘗試釋放共享模式下的一個讀鎖,如果釋放成功了,下一個讀鎖節點就也會被unparkSuccessor喚醒,然后也會獲得鎖。
如果釋放失敗了,那就把它的狀態標記了PROPAGATE,當它釋放的時候,會再次取嘗試喚醒下一個讀鎖節點
如果后繼節點是寫鎖,那么就不喚醒
?
?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) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
?private void setHeadAndPropagate(Node node, int propagate) {
??????? Node h = head; // Record old head for check below
??????? setHead(node);
???????
??????? if (propagate > 0 || h == null || h.waitStatus < 0) {
??????????? Node s = node.next;
??????????? if (s == null || s.isShared())
??????????????? 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;
??????? }
??? }
 AQS的FIFO隊列保證了在大量讀鎖和少量寫鎖的情況下,寫鎖也不會饑餓。
?
關于讀鎖能不能插隊的問題,非公平性的Sync提供了插隊的可能,但是前提是它在tryAcquire就成功獲得了,如果tryAcquire失敗了,它就得進入AQS隊列排隊,也不會出現讓寫鎖饑餓的情況。
?
關于寫鎖能不能插隊的情況,也是和讀鎖一樣,非公平的Sync提供了插隊的可能,如果tryAcquire獲取失敗,就得進入AQS等待。
?
最后說說為什么Semaphore和ReentrantLock在tryAcquireXX方法就實現了非公平性和公平性,而ReentrantReadWriteLock卻要抽象出readerShouldBlock和writerShouldBlock的方法來單獨處理公平性。
?
?abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();
 原因是Semaphore只支持共享模式,所以它只需要在NonfairSync和FairSync里面實現tryAcquireShared方法就能實現公平性和非公平性。
ReentrantLock只支持獨占模式,所以它只需要在NonfairSync和FairSync里面實現tryAcquire方法就能實現公平性和非公平性。
?
而ReentrantReadWriteLock即要支持共享和獨占模式,又要支持公平性和非公平性,所以它在基類的Sync里面用tryAcquire和tryAcquireShared方法來區分獨占和共享模式,
在NonfairSync和FairSync的readerShouldBlock和writerShouldBlock里面實現非公平性和公平性。
總結
以上是生活随笔為你收集整理的聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 聊聊高并发(二十八)解析java.uti
- 下一篇: 聊聊高并发(三十一)解析java.uti
