系列傳送門:
- Java併發包原始碼學習系列:AbstractQueuedSynchronizer
- Java併發包原始碼學習系列:CLH同步佇列及同步資源獲取與釋放
- Java併發包原始碼學習系列:AQS共享式與獨佔式獲取與釋放資源的區別
- Java併發包原始碼學習系列:ReentrantLock可重入獨佔鎖詳解
- Java併發包原始碼學習系列:ReentrantReadWriteLock讀寫鎖解析
- Java併發包原始碼學習系列:詳解Condition條件佇列、signal和await
- Java併發包原始碼學習系列:掛起與喚醒執行緒LockSupport工具類
- Java併發包原始碼學習系列:JDK1.8的ConcurrentHashMap原始碼解析
- Java併發包原始碼學習系列:阻塞佇列BlockingQueue及實現原理分析
- Java併發包原始碼學習系列:阻塞佇列實現之ArrayBlockingQueue原始碼解析
LinkedBlockingQueue概述
LinkedBlockingQueue是由單連結串列構成的界限可選的阻塞佇列,如不指定邊界,則為Integer.MAX_VALUE
,因此如不指定邊界,一般來說,插入的時候都會成功。
LinkedBlockingQueue支援FIFO先進先出的次序對元素進行排序。
類圖結構及重要欄位
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
private static final long serialVersionUID = -6903933977591709194L;
// 單連結串列節點
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
/** 容量,如果不指定就是Integer.MAX_VALUE */
private final int capacity;
/** 原子變數,記錄元素個數 */
private final AtomicInteger count = new AtomicInteger();
/**
* 哨兵頭節點,head.next才是佇列的第一個元素
*/
transient Node<E> head;
/**
* 指向最後一個元素
*/
private transient Node<E> last;
/** 用來控制同時只有一個執行緒可以從隊頭獲取元素 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 條件佇列,佇列為空時,執行出隊take操作的執行緒將會被置入該條件佇列 */
private final Condition notEmpty = takeLock.newCondition();
/** 用來控制同時只有一個執行緒可以從隊尾插入元素 */
private final ReentrantLock putLock = new ReentrantLock();
/** 條件佇列,佇列滿時,執行入隊操作put的執行緒將會被置入該條件佇列 */
private final Condition notFull = putLock.newCondition();
}
- 單向連結串列實現,維護head和last兩個Node節點,head是哨兵節點,head.next是第一個真正的元素,last指向隊尾節點。
- 佇列中的元素通過AtomicInteger型別的原子變數count記錄。
- 維護兩把鎖:takeLock保證同時只有一個執行緒可以從對頭獲取元素,putLock保證只有一個執行緒可以在隊尾插入元素。
- 維護兩個條件變數:notEmpty和notFull,維護條件佇列,用以存放入隊出隊阻塞的執行緒。
如果希望獲取一個元素,需要先獲取takeLock鎖,且notEmpty條件成立。
如果希望插入一個元素,需要先獲取putLock鎖,且notFull條件成立。
構造器
使用LinkedBlockingQueue的時候,可以指定容量,也可以使用預設的Integer.MAX_VALUE,幾乎就是無界的了,當然,也可以傳入集合物件,直接構造。
// 如果不指定容量,預設容量為Integer.MAX_VALUE (1 << 30) - 1
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
// 傳入指定的容量
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
// 初始化last 和 head指標
last = head = new Node<E>(null);
}
// 傳入指定集合物件,容量視為Integer.MAX_VALUE,直接構造queue
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
// 寫執行緒獲取putLock
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
出隊和入隊操作
佇列的操作最核心的部分莫過於入隊和出隊了,後面分析的方法基本上都基於這兩個工具方法。
LinkedBlockingQueue的出隊和入隊相對ArrayBlockingQueue來說就簡單很多啦:
入隊enqueue
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
- 將node連線到last的後面。
- 更新last指標的位置,指向node。
出隊dequeue
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first; // head向後移一位
E x = first.item;
first.item = null;
return x;
}
佇列中的元素實際上是從head.first開始的,那麼移除隊頭,其實就是將head指向head.next即可。
阻塞式操作
E take() 阻塞式獲取
take操作將會獲取當前佇列頭部元素並移除,如果佇列為空則阻塞當前執行緒直到佇列不為空,退出阻塞時返回獲取的元素。
如果執行緒在阻塞時被其他執行緒設定了中斷標誌,則丟擲InterruptedException異常並返回。
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
// 首先要獲取takeLock
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
// 如果佇列為空, notEmpty不滿足,就等著
while (count.get() == 0) {
notEmpty.await();
}
// 出隊
x = dequeue();
// c先賦值為count的值, count 減 1
c = count.getAndDecrement();
// 這次出隊後至少還有一個元素,喚醒notEmpty中的讀執行緒
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// c == capacity 表示在該元素出隊之前,佇列是滿的
if (c == capacity)
// 因為在這之前佇列是滿的,可能會有寫執行緒在等著,這裡做個喚醒
signalNotFull();
return x;
}
// 用於喚醒寫執行緒
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
// 獲取putLock
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
void put(E e) 阻塞式插入
put操作將向隊尾插入元素,如果佇列未滿則插入,如果佇列已滿,則阻塞當前執行緒直到佇列不滿。
如果執行緒在阻塞時被其他執行緒設定了中斷標誌,則丟擲InterruptedException異常並返回。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// 所有的插入操作 都約定 本地變數c 作為是否失敗的標識
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 插入操作獲取 putLock
putLock.lockInterruptibly();
try {
// 佇列滿,這時notFull條件不滿足,await
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
// c先返回count的值 , 原子變數 + 1 ,
c = count.getAndIncrement();
// 至少還有一個空位可以插入,notFull條件是滿足的,喚醒它
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// c == 0 表示在該元素入隊之前,佇列是空的
if (c == 0)
// 因為在這之前佇列是空的,可能會有讀執行緒在等著,這裡做個喚醒
signalNotEmpty();
}
// 用於喚醒讀執行緒
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
// 獲取takeLock
takeLock.lock();
try {
// 喚醒
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
E poll(timeout, unit) 阻塞式超時獲取
在take阻塞式獲取方法的基礎上額外增加超時功能,傳入一個timeout,獲取不到而阻塞的時候,如果時間到了,即使還獲取不到,也只能立即返回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) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
boolean offer(e, timeout, unit) 阻塞式超時插入
在put阻塞式插入方法的基礎上額外增加超時功能,傳入一個timeout,獲取不到而阻塞的時候,如果時間到了,即使還獲取不到,也只能立即返回null。
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) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(new Node<E>(e));
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
其他常規操作
boolean offer(E e)
offer(E e)是非阻塞的方法,向隊尾插入一個元素,如果佇列未滿,則插入成功並返回true;如果佇列已滿則丟棄當前元素,並返回false。
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
// 此時佇列已滿,直接返回false
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
// 插入操作 獲取putLock
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
// 加鎖後再校驗一次
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0; // 只要不是-1,就代表成功~
}
E poll()
從佇列頭部獲取並移除第一個元素,如果佇列為空則返回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 = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
E peek()
瞅一瞅隊頭的元素是啥,如果佇列為空,則返回null。
public E peek() {
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
// 實際上第一個元素是head.next
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
Boolean remove(Object o)
移除佇列中與元素o相等【指的是equals方法判定相同】的元素,移除成功返回true,如果佇列為空或沒有匹配元素,則返回false。
public boolean remove(Object o) {
if (o == null) return false;
fullyLock();
try {
// trail 和 p 同時向後遍歷, 如果p匹配了,就讓trail.next = p.next代表移除p
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;
}
}
return false;
} finally {
fullyUnlock();
}
}
// trail為p的前驅, 希望移除p節點
void unlink(Node<E> p, Node<E> trail) {
// assert isFullyLocked();
// p.next is not changed, to allow iterators that are
// traversing p to maintain their weak-consistency guarantee.
p.item = null;
trail.next = p.next;// 移除p
// 如果p已經是最後一個節點了,就更新一下last
if (last == p)
last = trail;
// 移除一個節點之後,佇列從滿到未滿, 喚醒notFull
if (count.getAndDecrement() == capacity)
notFull.signal();
}
//----- 多個鎖 獲取和釋放的順序是 相反的
// 同時上鎖
void fullyLock() {
putLock.lock();
takeLock.lock();
}
// 同時解鎖
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
總結
- LinkedBlockingQueue是由單連結串列構成的界限可選的阻塞佇列,如不指定邊界,則為
Integer.MAX_VALUE
,因此如不指定邊界,一般來說,插入的時候都會成功。 - 維護兩把鎖:takeLock保證同時只有一個執行緒可以從對頭獲取元素,putLock保證只有一個執行緒可以在隊尾插入元素。
- 維護兩個條件變數:notEmpty和notFull,維護條件佇列,用以存放入隊出隊阻塞的執行緒。
如果希望獲取一個元素,需要先獲取takeLock鎖,且notEmpty條件成立。
如果希望插入一個元素,需要先獲取putLock鎖,且notFull條件成立。
參考閱讀
- 《Java併發程式設計之美》
- 《Java併發程式設計的藝術》