本篇博文主要分析條件鎖的源碼實現、以及狀態兩個隊列的變化:
1)Condition的使用場景
2)lock方法的隊列(FIFO雙向無環鏈表)官方點說是同步隊列 sync queue
3)condition隊列(FIFO單向隊列) 官方點說是條件隊列 condition queue
4) await和signal方法被調用兩個隊列的變化圖
本文是依賴於上篇博文Java鎖Lock源碼分析(一)在閱讀本文之前強烈推薦先看下上一篇。
上篇主要主要是講述了Lock的幾個要點:
- state>0表示當前線程持有了鎖,以及重入鎖是如何表示的
2)併發的三個線程,獲取鎖過程、自旋的過程以及Node.waitStatus的狀態圖
3)源碼講解volatile、等待隊列(FIFO的雙向無環鏈表)的入隊和出隊
有了上一篇的基礎再讀本篇博文事半功倍,在說一點,本節主要以圖的形式展開(以爲代碼很簡答就是兩個對列,準確來說是一個雙向無環鏈表和一個單項無環鏈表的入隊和出隊操作,主要以圖的形式看下就可以了)。
Condition跟Object的相似之處:
condition.await()類比Object的wait()方法,
condition.signal()類比Object的notify()方法,
conditionsignalAll()類比Object的notifyAll()方法。
先獲取到鎖,然後在判斷條件是否滿足,不滿足則掛起,等待被喚醒
不同之處在於Object中的這些方法是需要跟同步監視器synchronized聯合使用,而Condition是Lock配合使用。
Condition能更細粒度的控制線程的休眠與喚醒,對於同一個鎖,我們可以創建多個Condition,來完成生產者和消費者的業務場景。
摘自 Doug Lea 的例子(網上到處是我直接拷貝過來),爲了更好的說明我們本次以4個線程 thread1/thread2/thread3/thread4 來展開源碼的分析。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class BoundedBuffer {
final Lock lock = new ReentrantLock();
// condition 依賴於 lock 來產生
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
// 生產
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 隊列已滿,等待,直到 not full 才能繼續生產
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 生產成功,隊列已經 not empty 了,發個通知出去
} finally {
lock.unlock();
}
}
// 消費
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 隊列爲空,等待,直到隊列 not empty,才能繼續消費
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 被我消費掉一個,隊列 not full 了,發個通知出去
return x;
} finally {
lock.unlock();
}
}
}
這了已經展示了條件鎖的使用,就是先lock()獲取鎖成功,讓後再while(一定是while判斷不能是if判斷,不然只要不滿足就永遠不滿足條件了)判斷,不滿足條件則condition.await();等待喚醒在此進入while判斷條件,獲取執行機會。
當向緩衝區中寫入數據之後,喚醒”讀線程”;
當從緩衝區讀出數據之後,喚醒”寫線程”,並且當緩衝區滿的時候,”寫線程”需要等待;
當緩衝區爲空時,”讀線程”需要等待。
如果採用Object類中的wait(), notify(), notifyAll()實現該緩衝區,當向緩衝區寫入數據之後需要喚醒”讀線程”時,不可能通過notify()或notifyAll()明確的指定喚醒”讀線程”,而只能通過notifyAll喚醒所有線程(但是notifyAll無法區分喚醒的線程是讀線程,還是寫線程)。 但是,通過Condition,就能明確的指定喚醒讀線程。
不知道大家還是是否有印象AQS的內部類Node
節點:
現在又多了一個內部類ConditionObject
,他是在成功獲取鎖了之後再看是否滿足條件,不滿足再次進入條件隊列阻塞,滿足的話執行相應的業務邏輯,兩個對列是如何交互的下文會將。
再講之前我們先模擬如下場景:
我們假定4個線程的併發執行lock.lock()方法執行的某個時間狀態是如下的:
thread1成功獲取了鎖【lock()返回】,但是數組已經滿了執行condition.await()【await()方法會將自己加入到條件隊列並釋放所後邊代碼會說明】,並將thread1掛起。
thread2在thread1釋放鎖後成功獲取了鎖【lock()返回】,數組也是滿了執行condition.await(),正在釋放鎖,即thread2正在調用fullRelease()方法。
thread3和thread4都獲取鎖失敗阻塞在lock方法中,在等待隊列中。
lock()方法的在上一篇文章Java鎖Lock源碼分析(一)寫過很詳細,這裏直接跳過看await()方法,它是AQS的另一個內部類ConditionObject的方法:
addConditionWaiter()就是創建一個條件隊列的節點,
new Node(thread,Node.CONDITION)將waitStatus設置爲-2,代碼如下:
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
這兩個截圖和上篇Java鎖Lock源碼分析(一) 就能得出如下的4個線程的狀態:
thread2和thread3都是:
獲取到了鎖AQS的state>1
不滿足執行業務邏輯條件創建條件Node,釋放鎖state歸爲0
我們thread2釋放鎖:
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
釋放鎖的邏輯就是設置state=0,喚醒雙向隊列的下一個未取消的節點thread3,thread3的前一個就是head,所以再次執行tryAcquire(1)成功,將自己設置爲頭結點head這樣隊列頭就被移除掉
,跟上一篇文章是一致的,,此時thread3獲取鎖成功,同樣條件不滿足進入await方法,兩個隊列的變化如下:
現在thread4和thread1線程都是掛起狀態的,thread2(釋放鎖完畢)和thread3正在釋放有可能正在掛起等,此時再有其它線程調用condition.signal()方法。我們在看下signal源碼:
方法很簡單就是將條件隊列的第一個未取消的節點移除掉,設置waitStatus=0,並放入到等待(同步)隊列中,將自己的waitStatus=-1,節點被喚醒執行一次執行await方法的後半部分所以會執行acquireQueued,將node.prev節點waitStatus=-1。
signalAll就是一次將所有的未取消的節點全部都挨個放入到等待隊列中然後執行跟signal方法相同的邏輯。
然後等待thread4獲取鎖(等待thread3釋放),thread4從同步隊列移除,在創建條件節點進入到條件隊列,直到thread1再次獲取了退出了await方法中的acquireQueued方法,才表示thread1的await方法退出,可以執行業務邏輯了。
值得注意的是都是同步隊列,thread1是在await方法中的acquireQueued方法阻塞,而thread4和thread5是在lock()方法中的acquireQueud方法中阻塞的,理解這個是非常重要的。
總的來說thread4和thread5是執行了兩次acquireQueued,第一次是沒有獲取到鎖,在隊列自旋(不是一直在循環佔用着CPU的之前文章說過),第二次是獲取到鎖,條件不滿足,等滿足被喚醒之後再執行acquireQueued。
這就是條件鎖的整個的邏輯,剩下的流程就lock.unlock(); 對於圖中表明的文字還是要看仔細,整個的流程還是比較複雜的。
在回顧一下之前的整個邏輯結合上一篇博文(理解這張圖就真的明白條件鎖了):
總結:
條件鎖是有兩個隊列同步隊列和條件隊列
隊列中存放的都是AQS的Node節點
同步隊列主要使用node.prev和node.next waitStatus默認是0 等待獲取鎖waitStatus=-1,初始化一個空的head
條件隊列主要是node.nextWaiter waitStatus=-2
lock方法阻塞的是進入到同步隊列,await方法第一次阻塞是在條件隊列(獲取鎖成功,但是不滿足條件,需要由signal方法喚醒),第二次阻塞是在同步隊列acquireQueue方法阻塞等待其他線程釋放鎖。
ps:上面的Doug Lea 大神的例子中,put和get方法都會調用了lock.lock()方法,這樣能達到一種情景就是:put方法和put方法互斥,get和get方法互斥,put和get互斥,很顯然get和get是互斥這樣會損失了性能的,能否做到寫方法和寫方法互斥,讀方法與寫方法互斥,但是讀方法與讀方法不互斥。這就是讀寫鎖來處理的事情了,下篇分析~!