LinkedBlockingQueue是一個基於已鏈接節點的、範圍任意的 blocking queue。此隊列按 FIFO(先進先出)排序元素。隊列的頭部 是在隊列中時間最長的元素。隊列的尾部 是在隊列中時間最短的元素。新元素插入到隊列的尾部,並且隊列獲取操作會獲得位於隊列頭部的元素。鏈接隊列的吞吐量通常要高於基於數組的隊列,但是在大多數併發應用程序中,其可預知的性能要低。
使用場景
LinkedBlockingQueue常用於生產者/消費者模式中,作爲生產者和消費者的通信橋樑。LinkedBlockingQueue與之前介紹的ConcurrentLinkedQueue以及PriorityBlockingQueue功能類似,都是Queue的一種,不同之處是:
- LinkedBlockingQueue和PriorityBlockingQueue是阻塞的,而ConcurrentLinkedQueue是非阻塞的,
- 同時LinkedBlockingQueue和PriorityBlockingQueue通過加鎖實現線程安全,而ConcurrentLinkedQueue使用CAS實現無鎖模式
- PriorityBlockingQueue支持優先級
由於不同的特徵,所以以上三者的使用場景也不同:
- LinkedBlockingQueue適合需要阻塞的隊列場景,如果能不阻塞或者可以通過代碼自行實現阻塞,那麼建議使用ConcurrentLinkedQueue代替
- ConcurrentLinkedQueue適合對性能要求較高,同時無需阻塞的場景使用
- PriorityBlockingQueue適合需要根據任務的不同優先級進行調整隊列的順序的場景
結構預覽
LinkedBlockingQueue內部實現相對較簡單,直接使用一個鏈表存儲數據,通過加鎖實現線程安全,通過兩個Condition分別實現入隊和出隊的等待。鏈表的節點使用內部類:Node表示,Node很簡單,就兩個變量,由外部類直接修改即可。
/**
* Linked list node class
*/
static class Node<E> {
/** The item, volatile to ensure barrier separating write and read */
volatile E item;
Node<E> next;
Node(E x) { item = x; }
}
item使用volatile修飾,解決內存可見性。
常用方法解析
LinkedBlockingQueue常用方法有:入隊(offer(E)/offer(E, long, TimeUnit)/put(E))、出隊(poll()/poll(long, TimeUnit)/take())、刪除(remove(Object))。下面分別看看這三類方法。
入隊
/**
* @By Vicky:入隊,無阻塞,隊列未滿則直接入隊,否則直接返回false
*/
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;// 保存當前隊列的長度
// 這裏因爲count是Atomic的,所以有類似volatile的內存可見性效果
// 即對count的修改能夠立即被其他線程可見,所以此處不加鎖的情況下讀取count值是會讀取到最新值的
// 然後根據此值進行前置判斷,避免不必要的加鎖操作
if (count.get() == capacity)// 隊列已滿直接返回false
return false;
int c = -1;
final ReentrantLock putLock = this.putLock;// 獲取putLock,加鎖
putLock.lock();
try {
if (count.get() < capacity) {// 隊列未滿則插入
insert(e);
c = count.getAndIncrement();// 更新count值
if (c + 1 < capacity)// 未滿則喚醒等待在notFull上的線程
// 此處有點怪異,入隊喚醒notFull~
// 此處喚醒notFull是考慮有可能如果多個線程同時出隊,由於出隊喚醒notFull時也需要對putLock進行加鎖
// 所以有可能一個線程出隊,喚醒notFull,但是被另一個出隊線程搶到了鎖,所以入隊線程依舊在等待
// 當另一個線程也喚醒了notFull,釋放了putLock後,只能喚醒一個入隊線程,所以其他線程依舊在等待
// 所以此處需要再次喚醒notFull
notFull.signal();
}
} finally {
putLock.unlock();
}
// c==0表示隊列在插入之前是空的,所以需要喚醒等待在notEmpty上的線程
if (c == 0)
signalNotEmpty();
return c >= 0;
}
/**
* @By Vicky:喚醒notEmpty,需對takeLock進行加鎖,因爲notEmpty與takeLock相關
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
首先解析offer(),另外兩個入隊操作只是在隊列已滿的情況下進行一些特殊處理而已。文中代碼給出了詳細註釋,這裏着重說明兩個地方:
- 對Condition的操作需要在加鎖的環境下進行,而且是需要對與Condition相關的鎖進行加鎖,如此處notEmpty是由takeLock.newCondition()得來,所以對notEmpty的操作需要對takeLock進行加鎖
- 入隊操作也執行
notFull.signal();
的原因是避免入隊線程未搶到鎖而遺失了出隊的喚醒操作。詳細解析可以見文中的註釋
下面直接貼出offer(E, long, TimeUnit)和put(E)的代碼,基本同offer(E)。
/**
* @By Vicky:入隊,等待指定時間
*/
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 {
for (;;) {
// 此處同offer()
if (count.get() < capacity) {
insert(e);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
break;
}
// nanos是剩餘的等待時間,<=0表示等待時間已到
if (nanos <= 0)
return false;
try {
// 調用notFull的awaitNanos,指定等待時間,如果等待期間被喚醒,則返回剩餘等待時間,<0表示等待時間已到
nanos = notFull.awaitNanos(nanos);
} catch (InterruptedException ie) {
notFull.signal(); // propagate to a non-interrupted thread
throw ie;
}
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
/**
* @By Vicky:入隊,無期限等待
*/
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset
// local var holding count negative to indicate failure unless set.
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
try {
while (count.get() == capacity)// 無限等待,直到可用
notFull.await();
} catch (InterruptedException ie) {
notFull.signal(); // propagate to a non-interrupted thread
throw ie;
}
insert(e);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
出隊
出隊操作和入隊邏輯相同,看代碼。
/**
* @By Vicky:出隊,無阻塞,隊列爲空則直接返回null
*/
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)// 隊列爲空,直接返回
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {// 不爲空,獲取一個元素
x = extract();
c = count.getAndDecrement();
if (c > 1)// 同offer(),此處需喚醒notEmpty
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();// 同offer(),此處需喚醒notFull
return x;
}
/**
* @By Vicky:出隊,將head指向head.next
* @return
*/
private E extract() {
Node<E> first = head.next;
head = first;
E x = first.item;
first.item = null;
return x;
}
/**
* @By Vicky:喚醒notFull,需對putLock進行加鎖,因爲notFull與putLock相關
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
出隊一個元素:extract()
,邏輯很簡單,將head指向head.next即可。其他地方與offer()的邏輯相同,如隊列未空需喚醒notEmpty,隊列由滿變空需喚醒notFull,原因完全同offer()。poll(long, TimeUnit)和take()代碼就不貼出來了,完全與offer()相同。
刪除
/**
* @By Vicky:刪除指定元素
*/
public boolean remove(Object o) {
if (o == null) return false;
boolean removed = false;
fullyLock();// 同時對takeLock和pullLock加鎖,避免任何的入隊和出隊操作
try {
Node<E> trail = head;
Node<E> p = head.next;
while (p != null) {// 從隊列的head開始循環查找與o相同的元素
if (o.equals(p.item)) {// 找到相同的元素則設置remove爲true
removed = true;
break;
}
trail = p;// 繼續循環
p = p.next;
}
if (removed) {
// remove==true,則表示查找到待刪除元素,即p,將trail的next指向p的next,即將p從隊列移除及完成刪除
p.item = null;
trail.next = p.next;
if (last == p)
last = trail;
if (count.getAndDecrement() == capacity)
notFull.signalAll();
}
} finally {
fullyUnlock();
}
return removed;
}
刪除的邏輯也很簡單,代碼中給出了註釋。
以上即本篇全部內容,比較簡單,更多關於隊列的研究可參考:
以上內容如有錯誤,請不吝賜教~