我在前段時間寫了一篇關於AQS原始碼解析的文章AbstractQueuedSynchronizer超詳細原理解析
,在文章裡邊我說JUC
包中的大部分多執行緒相關的類都和AQS
相關,今天我們就學習一下依賴於AQS
來實現的阻塞佇列BlockingQueue
的實現原理。本文中的原始碼未加說明即來自於以ArrayBlockingQueue
。
阻塞佇列
相信大多數同學在學習執行緒池時會了解阻塞佇列的概念,熟記各種型別的阻塞佇列對執行緒池初始化的影響。當從阻塞佇列獲取元素但是佇列為空時,當前執行緒會阻塞直到另一個執行緒向阻塞佇列中新增一個元素;類似的,當向一個阻塞佇列加入元素時,如果佇列已經滿了,當前執行緒也會阻塞直到另外一個執行緒從佇列中讀取一個元素。阻塞佇列一般都是先進先出的,用來實現生產者和消費者模式。當發生上述兩種情況時,阻塞佇列有四種不同的處理方式,這四種方式分別為丟擲異常,返回特殊值(null或在是false),阻塞當前執行緒直到執行結束,最後一種是隻阻塞固定時間,到時後還無法執行成功就放棄操作。這些方法都總結在下邊這種表中了。
我們就只分析put
和take
方法。
put和take函式
我們都知道,使用同步佇列可以很輕鬆的實現生產者-消費者模式,其實,同步佇列就是按照生產者-消費者的模式來實現的,我們可以將put
函式看作生產者的操作,take
是消費者的操作。
我們首先看一下ArrayListBlock
的建構函式。它初始化了put
和take
函式中使用到的關鍵成員變數,分別是ReentrantLock
和Condition
。
public ArrayBlockingQueue(int capacity, boolean fair) {
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
複製程式碼
ReentrantLock是AQS
的子類,其newCondition
函式返回的Condition
介面例項是定義在AQS類內部的ConditionObject
實現類。它可以直接呼叫AQS
相關的函式。
put
函式會在佇列末尾新增元素,如果佇列已經滿了,無法新增元素的話,就一直阻塞等待到可以加入為止。函式的原始碼如下所示。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //先獲得鎖
try {
while (count == items.length)
//如果佇列滿了,就NotFull這個Condition物件上進行等待
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
//這裡可以注意的是ArrayBlockingList實際上使用Array實現了一個環形陣列,
//當putIndex達到最大時,就返回到起點,繼續插入,
//當然,如果此時0位置的元素還沒有被取走,
//下次put時,就會因為cout == item.length未被阻塞。
if (++putIndex == items.length)
putIndex = 0;
count++;
//因為插入了元素,通知等待notEmpty事件的執行緒。
notEmpty.signal();
}
複製程式碼
我們會發現put函式使用了wait/notify的機制。與一般生產者-消費者的實現方式不同,同步佇列使用ReentrantLock
和Condition
相結合的先獲得鎖,再等待的機制;而不是Synchronized
和Object.wait
的機制。這裡的區別我們下一節再詳細講解。
看完了生產者相關的put
函式,我們再來看一下消費者呼叫的take
函式。take
函式在佇列為空時會被阻塞,一直到阻塞佇列加入了新的元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
//如果佇列為空,那麼在notEmpty物件上等待,
//當put函式呼叫時,會呼叫notEmpty的notify進行通知。
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
E x = (E) items[takeIndex];
items[takeIndex] = null; //取出takeIndex位置的元素
if (++takeIndex == items.length)
//如果到了尾部,將指標重新調整到頭部
takeIndex = 0;
count--;
....
//通知notFull物件上等待的執行緒
notFull.signal();
return x;
}
複製程式碼
await操作
我們發現ArrayBlockingList
並沒有使用Object.wait
,而是使用的Condition.await
,這是為什麼呢?其中又有哪些原因呢?
Condition
物件可以提供和Object
的wait
和notify
一樣的行為,但是後者必須先獲取synchronized
這個內建的monitor鎖,才能呼叫;而Condition
則必須先獲取ReentrantLock
。這兩種方式在阻塞等待時都會將相應的鎖釋放掉,但是Condition
的等待可以中斷,這是二者唯一的區別。
我們先來看一下Condition
的wait
函式,wait
函式的流程大致如下圖所示。
wait
函式主要有三個步驟。一是呼叫addConditionWaiter
函式,在condition wait queue佇列中新增一個節點,代表當前執行緒在等待一個訊息。然後呼叫fullyRelease
函式,將持有的鎖釋放掉,呼叫的是AQS的函式,不清楚的同學可以檢視本篇開頭的介紹的文章。最後一直呼叫isOnSyncQueue
函式判斷節點是否被轉移到sync queue
佇列上,也就是AQS中等待獲取鎖的佇列。如果沒有,則進入阻塞狀態,如果已經在佇列上,則呼叫acquireQueued
函式重新獲取鎖。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//在condition wait佇列上新增新的節點
Node node = addConditionWaiter();
//釋放當前持有的鎖
int savedState = fullyRelease(node);
int interruptMode = 0;
//由於node在之前是新增到condition wait queue上的,現在判斷這個node
//是否被新增到Sync的獲得鎖的等待佇列上,Sync就是AQS的子類
//node在condition queue上說明還在等待事件的notify,
//notify函式會將condition queue 上的node轉化到Sync的佇列上。
while (!isOnSyncQueue(node)) {
//node還沒有被新增到Sync Queue上,說明還在等待事件通知
//所以呼叫park函式來停止執行緒執行
LockSupport.park(this);
//判斷是否被中斷,執行緒從park函式返回有兩種情況,一種是
//其他執行緒呼叫了unpark,另外一種是執行緒被中斷
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//程式碼執行到這裡,已經有其他執行緒呼叫notify函式,或則被中斷,該執行緒可以繼續執行,但是必須先
//再次獲得呼叫await函式時的鎖.acquireQueued函式在AQS文章中做了介紹.
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
 ....
}
final int fullyRelease(Node node) {
//AQS的方法,當前已經在鎖中了,所以直接操作
boolean failed = true;
try {
int savedState = getState();
//獲取state當前的值,然後儲存,以待以後恢復
// release函式是AQS的函式,不清楚的同學請看開頭介紹的文章。
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
private int checkInterruptWhileWaiting(Node node) {
//中斷可能發生在兩個階段中,一是在等待signa時,另外一個是在獲得signal之後
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
final boolean transferAfterCancelledWait(Node node) {
//這裡要和下邊的transferForSignal對應著看,這是執行緒中斷進入的邏輯.那邊是signal的邏輯
//兩邊可能有併發衝突,但是成功的一方必須呼叫enq來進入acquire lock queue中.
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
//如果失敗了,說明transferForSignal那邊成功了,等待node 進入acquire lock queue
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
複製程式碼
signal操作
signal
函式將condition wait queue
佇列中隊首的執行緒節點轉移等待獲取鎖的sync queue
佇列中。這樣的話,wait
函式中呼叫isOnSyncQueue
函式就會返回true,導致wait
函式進入最後一步重新獲取鎖的狀態。
我們這裡來詳細解析一下condition wait queue
和sync queue
兩個佇列的設計原理。condition wait queue
是等待訊息的佇列,因為阻塞佇列為空而進入阻塞狀態的take
函式操作就是在等待阻塞佇列不為空的訊息。而sync queue
佇列則是等待獲取鎖的佇列,take函式獲得了訊息,就可以執行了,但是它還必須等待獲取鎖之後才能真正進行執行狀態。
signal
函式的示意圖如下所示。
signal
函式其實就做了一件事情,就是不斷嘗試呼叫transferForSignal
函式,將condition wait queue
隊首的一個節點轉移到sync queue
佇列中,直到轉移成功。因為一次轉移成功,就代表這個訊息被成功通知到了等待訊息的節點。
public final void signal() {
if (!isHeldExclusively())
//如果當前執行緒沒有獲得鎖,丟擲異常
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
//將Condition wait queue中的第一個node轉移到acquire lock queue中.
doSignal(first);
}
private void doSignal(Node first) {
do {
   //由於生產者的signal在有消費者等待的情況下,必須要通知
//一個消費者,所以這裡有一個迴圈,直到佇列為空
//把first 這個node從condition queue中刪除掉
//condition queue的頭指標指向node的後繼節點,如果node後續節點為null,那麼也將尾指標也置為null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
//transferForSignal將node轉而新增到Sync的acquire lock 佇列
}
final boolean transferForSignal(Node node) {
//如果設定失敗,說明該node已經被取消了,所以返回false,讓doSignal繼續向下通知其他未被取消的node
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//將node新增到acquire lock queue中.
Node p = enq(node);
int ws = p.waitStatus;
//需要注意的是這裡的node進行了轉化
//ws>0代表canceled的含義所以直接unpark執行緒
//如果compareAndSetWaitStatus失敗,所以直接unpark,讓執行緒繼續執行await中的
//進行isOnSyncQueue判斷的while迴圈,然後進入acquireQueue函式.
//這裡失敗的原因可能是Lock其他執行緒釋放掉了鎖,同步設定p的waitStatus
//如果compareAndSetWaitStatus成功了呢?那麼該node就一直在acquire lock queue中
//等待鎖被釋放掉再次搶奪鎖,然後再unpark
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
複製程式碼
後記
後邊一篇文章主要講解如何自己使用AQS
來建立符合自己業務需求的鎖,請大家繼續關注我的文章啦.一起進步偶。