一、前言
前面介紹了使用CAS實現的非阻塞隊列ConcurrentLinkedQueue,下面就來介紹下使用獨占鎖實現的阻塞隊列LinkedBlockingQueue的實現
阿里巴巴長期招聘Java研發工程師p6,p7,p8等上不封頂級別,有意向的可以發簡歷給我,注明想去的部門和工作地點:1064454834@qq.com
二、 LinkedBlockingQueue類圖結構
如圖LinkedBlockingQueue中也有兩個Node分別用來存放首尾節點,并且里面有個初始值為0的原子變量count用來記錄隊列元素個數,另外里面有兩個ReentrantLock的獨占鎖,分別用來控制元素入隊和出隊加鎖,其中takeLock用來控制同時只有一個線程可以從隊列獲取元素,其他線程必須等待,putLock控制同時只能有一個線程可以獲取鎖去添加元素,其他線程必須等待。另外notEmpty和notFull用來實現入隊和出隊的同步。 另外由于出入隊是兩個非公平獨占鎖,所以可以同時又一個線程入隊和一個線程出隊,其實這個是個生產者-消費者模型。
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger(0);
public static final int MAX_VALUE = 0x7fffffff;
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
//初始化首尾節點
last = head = new Node<E>(null);
}
如圖默認隊列容量為0x7fffffff;用戶也可以自己指定容量。
三、必備基礎
3.1 ReentrantLock
可以參考 https://www.atatech.org/articles/80539?flag_data_from=active
3.2 條件變量(Condition)
條件變量這里使用的是takeLock.newCondition()獲取也就是說調用ReentrantLock的方法獲取的,那么可預見Condition使用了ReentrantLock的state。上面的參考沒有提到所以這里串串講下
- 首先看下類圖結構
如圖ConditionObject中兩個node分別用來存放條件隊列的首尾節點,條件隊列就是調用條件變量的await方法被阻塞后的節點組成的單向鏈表。另外ConditionObject還要依賴AQS的state,ConditionObject是AQS類的一個內部類。
- awaitNanos操作
public final long awaitNanos(long nanosTimeout)
throws InterruptedException {
//如果中斷標志被設置了,則拋異常
if (Thread.interrupted())
throw new InterruptedException();
//添加當前線程節點到條件隊列,
Node node = addConditionWaiter();
//當前線程釋放獨占鎖
int savedState = fullyRelease(node);
long lastTime = System.nanoTime();
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
if (nanosTimeout <= 0L) {
transferAfterCancelledWait(node);
break;
}
//掛起當前線程直到超時
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
long now = System.nanoTime();
nanosTimeout -= now - lastTime;
lastTime = now;
}
//unpark后,當前線程重新獲取鎖,有可能獲取不到被放到AQS的隊列
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return nanosTimeout - (System.nanoTime() - lastTime);
}
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;
}
}
首先如果當前線程中斷標志被設置了,直接拋出異常。添加當前線程節點(狀態為:-2)到條件隊列。
然后嘗試釋放當前線程擁有的鎖并保存當前計數,可知如果當前線程調用awaitNano前沒有使用當前條件變量所在的Reetenlock變量調用lock或者lockInterruptibly獲取到鎖,會拋出IllegalMonitorStateException異常。
然后調用park掛起當前線程直到超時或者其他線程調用了當前線程的unpark方法,或者調用了當前線程的interupt方法(這時候會拋異常)。
如果超時或者其他線程調用了當前線程的unpark方法,則當前線程從掛起變為激活,獲取cpu資源后會繼續執行,會重新獲取鎖。
- signal操作
public final void signal() {
//如果當前線程沒有持有鎖,拋異常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//從條件隊列找第一個狀態為CONDITION的,然后把狀態變為0
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//狀態為CONDITION的,然后把狀態變為0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//把條件隊列的上面狀態為0的節點放入AQS阻塞隊列
Node p = enq(node);
int ws = p.waitStatus;
//調用unpark激活掛起的線程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
首先看調用signal的線程是不是持有了獨占鎖,沒有則拋出異常。
然后獲取在條件隊列里面待的時間最長的node,把它移動到線程持有的鎖所在的AQS隊列。
其中enq方法就是把當前節點放入了AQS隊列,但是這時候該節點還是在條件隊列里面那,那么什么時候從條件隊列移除那?其實在await里面的unlinkCancelledWaiters方法。
總結:
無論是條件變量的await和singal都是需要先獲取獨占鎖才能調用,因為條件變量使用的就是獨占鎖里面的state管理狀態,否者會報異常。
四 、帶超時時間的offer操作-生產者
在隊尾添加元素,如果隊列滿了,那么等待timeout時候,如果時間超時則返回false,如果在超時前隊列有空余空間,則插入后返回true。
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
//空元素拋空指針異常
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//獲取可被中斷鎖,只有一個線程克獲取
putLock.lockInterruptibly();
try {
//如果隊列滿則進入循環
while (count.get() == capacity) {
//nanos<=0直接返回
if (nanos <= 0)
return false;
//否者調用await進行等待,超時則返回<=0(1)
nanos = notFull.awaitNanos(nanos);
}
//await在超時時間內返回則添加元素(2)
enqueue(new Node<E>(e));
c = count.getAndIncrement();
//隊列不滿則激活其他等待入隊線程(3)
if (c + 1 < capacity)
notFull.signal();
} finally {
//釋放鎖
putLock.unlock();
}
//c==0說明隊列里面有一個元素,這時候喚醒出隊線程(4)
if (c == 0)
signalNotEmpty();
return true;
}
private void enqueue(Node<E> node) {
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
如果獲取鎖前面有線程調用了putLock. interrupt(),并且后面沒有調用interrupted()重置中斷標志,調用lockInterruptibly時候會拋出InterruptedException異常。
隊列滿的時候調用notFull.awaitNanos阻塞當前線程,當前線程會釋放獲取的鎖,然后等待超時或者其他線程調用了notFull.signal()才會返回并重新獲取鎖,或者其他線程調用了該線程的interrupt方法設置了中斷標志,這時候也會返回但是會拋出InterruptedException異常。
如果超時則直接返回false,如果超時前調用了notFull.signal()則會退出循環,執行(2)添加元素到隊列,然后執行(3),(3)的目的是為了激活其他入隊等待線程。(4)的話c==0說明隊列里面已經有一個元素了,這時候就可以激活等待出隊線程了。
另外signalNotEmpty函數是先獲取獨占鎖,然后在調用的signal這也證明了3.2節的結論。
五、 帶超時時間的poll操作-消費者
獲取并移除隊首元素,在指定的時間內去輪詢隊列看有沒有首元素有則返回,否者超時后返回null
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//出隊線程獲取獨占鎖
takeLock.lockInterruptibly();
try {
//循環直到隊列不為空
while (count.get() == 0) {
//超時直接返回null
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
//出隊,計數器減一
x = dequeue();
c = count.getAndDecrement();
//如果出隊前隊列不為空則發送信號,激活其他阻塞的出隊線程
if (c > 1)
notEmpty.signal();
} finally {
//釋放鎖
takeLock.unlock();
}
//當前隊列容量為最大值-1則激活入隊線程。
if (c == capacity)
signalNotFull();
return x;
}
首先獲取獨占鎖,然后進入循環當當前隊列有元素才會退出循環,或者超時了,直接返回null。
超時前退出循環后,就從隊列移除元素,然后計數器減去一,如果減去1前隊列元素大于1則說明當前移除后隊列還有元素,那么就發信號激活其他可能阻塞到當前條件信號的線程。
最后如果減去1前隊列元素個數=最大值,那么移除一個后會騰出一個空間來,這時候可以激活可能存在的入隊阻塞線程。
六、put操作-生產者
與帶超時時間的poll類似不同在于put時候如果當前隊列滿了它會一直等待其他線程調用notFull.signal才會被喚醒。
七、 take操作-消費者
與帶超時時間的poll類似不同在于take時候如果當前隊列空了它會一直等待其他線程調用notEmpty.signal()才會被喚醒。
八、 size操作
當前隊列元素個數,如代碼直接使用原子變量count獲取。
public int size() {
return count.get();
}
九、peek操作
獲取但是不移除當前隊列的頭元素,沒有則返回null
public E peek() {
//隊列空,則返回null
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
十、 remove操作
刪除隊列里面的一個元素,有則刪除返回true,沒有則返回false,在刪除操作時候由于要遍歷隊列所以加了雙重鎖,也就是在刪除過程中不允許入隊也不允許出隊操作
public boolean remove(Object o) {
if (o == null) return false;
//雙重加鎖
fullyLock();
try {
//遍歷隊列找則刪除返回true
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
//找不到返回false
return false;
} finally {
//解鎖
fullyUnlock();
}
}
void fullyLock() {
putLock.lock();
takeLock.lock();
}
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
void unlink(Node<E> p, Node<E> trail) {
p.item = null;
trail.next = p.next;
if (last == p)
last = trail;
//如果當前隊列滿,刪除后,也不忘記最快的喚醒等待的線程
if (count.getAndDecrement() == capacity)
notFull.signal();
}
十一、開源框架中使用
tomcat中任務隊列TaskQueue
11.1 類圖結構
可知TaskQueue繼承了LinkedBlockingQueue并且泛化類型固定了為Runnalbe.重寫了offer,poll,take方法。
11.2 TaskQueue
tomcat中有個線程池ThreadPoolExecutor,在NIOEndPoint中當acceptor線程接受到請求后,會把任務放入隊列,然后poller 線程從隊列里面獲取任務,然后就吧任務放入線程池執行。這個ThreadPoolExecutor中的的一個參數就是TaskQueue。
先看看ThreadPoolExecutor的參數如果是普通LinkedBlockingQueue是怎么樣的執行邏輯:
當調用線程池方法 execute() 方法添加一個任務時:
- 如果當前運行的線程數量小于 corePoolSize,則創建新線程運行該任務
- 如果當前運行的線程數量大于或等于 corePoolSize,則將這個任務放入阻塞隊列。
- 如果當前隊列滿了,并且當前運行的線程數量小于 maximumPoolSize,則創建新線程運行該任務;
- 如果當前隊列滿了,并且當前運行的線程數量大于或等于 maximumPoolSize,那么線程池將會拋出RejectedExecutionException異常。
如果線程執行完了當前任務,那么會去隊列里面獲取一個任務來執行,如果任務執行完了,并且當前線程數大于corePoolSize,那么會根據線程空閑時間keepAliveTime回收一些線程保持線程池corePoolSize個線程。
首先看下線程池中exectue添加任務時候的邏輯:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//當前工作線程個數小于core個數則開新線程執行(1)
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//放入隊列(2)
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果隊列滿了則開新線程,但是個數要不超過最大值,超過則返回false
//然后執行reject handler(3)
else if (!addWorker(command, false))
reject(command);
}
可知當當前工作線程個數為corePoolSize后,如果在來任務會把任務添加到隊列,隊列滿了或者入隊失敗了則開啟新線程。
然后看看TaskQueue中重寫的offer方法的邏輯:
public boolean offer(Runnable o) {
// 如果parent為null則直接調用父類方法
if (parent==null) return super.offer(o);
//如果當前線程池中線程個數達到最大,則無條件調用父類方法
if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
//如果當前提交的任務小于當前線程池線程數,說明線程用不完,沒必要重新開線程
if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o);
//如果當前線程池線程個數>core個數但是小于最大個數,則開新線程代替放入隊列
if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
//到了這里,無條件調用父類
return super.offer(o);
}
可知parent.getPoolSize()<parent.getMaximumPoolSize()普通隊列會把當前任務放入隊列,TAskQueue則是返回false,因為這會開啟新線程執行任務,當然前提是當前線程個數沒有達到最大值。
然后看下Worker線程中如果從隊列里面獲取任務執行的:
final void runWorker(Worker w) {
...
try {
while (task != null || (task = getTask()) != null) {
...
}
completedAbruptly = false;
} finally {
...
}
}
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
...
int wc = workerCountOf(c);
...
try {
//根據timed決定調用poll還是take
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
十二、總結
12.1 并發安全總結
仔細思考下阻塞隊列是如何實現并發安全的維護隊列鏈表的,先分析下簡單的情況就是當隊列里面有多個元素時候,由于同時只有一個線程(通過獨占鎖putLock實現)入隊元素并且是操作last節點(,而同時只有一個出隊線程(通過獨占鎖takeLock實現)操作head節點,所以不存在并發安全問題。
- 考慮當隊列為空的時候隊列狀態為:
這時候假如一個線程調用了take方法,由于隊列為空,所以count.get()==0所以當前線程會調用notEmpty.await()把自己掛起,并且放入notEmpty的條件隊列,并且釋放當前條件變量關聯的通過takeLock.lockInterruptibly()獲取的獨占鎖。由于釋放了鎖,所以這時候其他線程調用take時候就會通過takeLock.lockInterruptibly()獲取獨占鎖,然后同樣阻塞到notEmpty.await(),同樣會被放入notEmpty的條件隊列,也就說在隊列為空的情況下可能會有多個線程因為調用take被放入了notEmpty的條件隊列。
這時候如果有一個線程調用了put方法,那么就會調用enqueue操作,該操作會在last節點后面添加新元素并且設置last為新節點。然后count.getAndIncrement()先獲取當前隊列元個數為0保存到c,然后自增count為1,由于c==0所以調用signalNotEmpty激活notEmpty的條件隊列里面的阻塞時間最長的線程,這時候take中調用notEmpty.await()的線程會被激活await內部會重新去獲取獨占鎖獲取成功則返回,否者被放入AQS的阻塞隊列,如果獲取成功,那么count.get() >0因為可能多個線程put了,所以調用dequeue從隊列獲取元素(這時候一定可以獲取到),然后調用c = count.getAndDecrement() 把當前計數返回后并減去1,如果c>1 說明當前隊列還有其他元素,那么就調用 notEmpty.signal()去激活 notEmpty的條件隊列里面的其他阻塞線程。
- 考慮當隊列滿的時候:
當隊列滿的時候調用put方法時候,會由于notFull.await()當前線程被阻塞放入notFull管理的條件隊列里面,同理可能會有多個調用put方法的線程都放到了notFull的條件隊列里面。
這時候如果有一個線程調用了take方法,調用dequeue()出隊一個元素,c = count.getAndDecrement();count值減一;c==capacity;現在隊列有一個空的位置,所以調用signalNotFull()激活notFull條件隊列里面等待最久的一個線程。
12.2簡單對比
LinkedBlockingQueue與ConcurrentLinkedQueue相比前者前者是阻塞隊列使用可重入獨占的非公平鎖來實現通過使用put鎖和take鎖使得入隊和出隊解耦可以同時進行處理,但是同時只有一個線程可以入隊或者出隊,其他線程必須等待,另外引入了條件變量來進行入隊和出隊的同步,每個條件變量維護一個條件隊列用來存放阻塞的線程,要注意這個隊列和AQS的隊列不是一個東東。LinkedBlockingQueue的size操作通過使用原子變量count獲取能夠比較精確的獲取當前隊列的元素個數,另外remove方法使用雙鎖保證刪除時候隊列元素保持不變,另外其實這個是個生產者-消費者模型。
而ConcurrentLinkedQueue則使用CAS非阻塞算法來實現,使用CAS原子操作保證鏈表構建的安全性,當多個線程并發時候CAS失敗的線程不會被阻塞,而是使用cpu資源去輪詢CAS直到成功,size方法先比LinkedBlockingQueue的獲取的個數是不精確的,因為獲取size的時候是通過遍歷隊列進行的,而遍歷過程中可能進行增加刪除操作,remove方法操作時候也沒有對整個隊列加鎖,remove時候可能進行增加刪除操作,這就可能刪除了一個剛剛新增的元素,而不是刪除的想要位置的。
歡迎關注微信公眾號:‘技術原始積累’ 獲取更多技術干貨__