什麼是阻塞隊列?
阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作是:
- 在隊列爲空時,獲取元素的線程會阻塞等待,直到隊列變爲非空或超時。
- 當隊列滿時,存儲元素的線程會等待隊列可用。
阻塞隊列常用於生產者和消費者的場景。
阻塞隊列提供了四種處理方法:
- 拋出異常:是指當阻塞隊列滿時候,再往隊列裏插入元素,會拋出IllegalStateException(“Queue full”)異常。當隊列爲空時,從隊列裏獲取元素時會拋出NoSuchElementException異常 。
- 返回特殊值:插入方法會返回是否成功,成功則返回true。移除方法,則是從隊列裏拿出一個元素,如果沒有則返回null
- 一直阻塞:當阻塞隊列滿時,如果生產者線程往隊列裏put元素,隊列會一直阻塞生產者線程,直到拿到數據,或者響應中斷退出。當隊列空時,消費者線程試圖從隊列裏take元素,隊列也會阻塞消費者線程,直到隊列可用。
- 超時退出:當阻塞隊列滿時,隊列會阻塞生產者線程一段時間,如果超過一定的時間,生產者線程就會退出。
ArrayBlockingQueue
ArrayBlockingQueue的內部是通過一個可重入鎖ReentrantLock和兩個Condition條件對象來實現阻塞。
關於 ReentrantLock
關於 Condition
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 存儲數據的數組
final Object[] items;
// 獲取數據的索引,用於take,poll,remove等方法
int takeIndex;
// 添加數據的索引,用於 put, offer, add等方法
int putIndex;
// 元素個數
int count;
// 鎖
final ReentrantLock lock;
// Condition 內有個隊連,有別於AQS的同步隊列,我們稱它爲等待隊列
// await 會先構建當前線程的節點放入等待隊列,之後阻塞線程
// signal 一般不會喚醒線程,而是將節點放回AQS的同步隊列,等待被喚醒
// notEmpty隊列裏放的是執行獲取操作的線程,它們之前由於數組爲空無法執行獲取操作而阻塞
// 執行取操作的線程由於數組爲空而被放入notEmpty的等待隊列中等待
// 當執行添加操作的線程執行成功後調用notEmpty.signal將notEmpty隊列中的
// 頭節點放回AQS的同步隊列,其代表的線程在隊列中等待被喚醒
private final Condition notEmpty;
// notFull 的隊列中放的是執行插入操作的線程,它們之前由於數組滿無法執行添加操作而阻塞
private final Condition notFull;
// 迭代器
transient Itrs itrs = null;
在上面提到了AQS的同步隊列與Condition的等待隊列,它們在我的關於 ReentrantLock 和 Condition兩篇源碼分析文章中有詳細介紹。
這裏貼張圖比那與理解:
第一條隊列是AQS的同步隊列,下面是Condition的等待隊列。
關於 notEmpty 與 notFull 不太好記憶,可以將 notEmpty 與 get 聯繫在一起;notFull 與 put 聯繫在一起。
add,offer添加操作
public boolean add(E e) {
return super.add(e);
}
// 跳到AbstractQueue:
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
// offer方法在ArrayBlockingQueue的實現
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
對重點進行下總結:
- 添加操作先獲取鎖;
- add 方法在數組滿時拋異常,offer 則返回 false
- 喚醒一個notEmpty等待隊列中的線程,該隊列中的線程都是執行獲取操作的
put操作
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// lockInterruptibly相對於lock,就如其名一樣,檢測到中斷就直接拋異常
lock.lockInterruptibly();
try {
while (count == items.length)
// 數組滿無法插入,將插入線程放入notFull等待隊列
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
可以看出 put 有別於 add,數組滿就等待而不是拋異常。
remove,poll刪除操作
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
// takeIndex位置元素被刪除,需根據情況對迭代器鏈上所有迭代器進行處理
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
- 數組爲空時,remove 拋異常,poll 返回 null 。
- 刪除成功後將 notFull 中一個插入線程的節點放回同步隊列,在隊列中輪到它時就被喚醒執行插入操作,所謂輪到它指的是在同步隊列中排在它之前的節點都被一一喚醒。
- 元素刪除後可能會對一些迭代器造成影響,這裏需要處理這種影響。
阻塞隊列的迭代器實現原理分析我專門寫了兩篇分析:
深入Java併發之阻塞隊列-迭代器(一)
深入Java併發之阻塞隊列-迭代器(二)
take 阻塞獲取
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 中斷拋異常
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
首先要獲取鎖,數組爲空則放入 notEmpty 等待隊列。
element,peek
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
數組爲空 peek 返回 null;element 則拋異常。
LinkedBlockingQueue
LinkedBlockingQueue內部分別使用了 takeLock 和 putLock 兩個鎖對併發進行控制,添加和刪除操作並不是互斥操作,可以同時進行,這樣也就可以大大提高吞吐量。
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
節點
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();
//頭節點
transient Node<E> head;
//尾節點
private transient Node<E> last;
// take,poll等取操作的鎖
private final ReentrantLock takeLock = new ReentrantLock();
// 數組爲空無法執行取操作的線程被放入 notEmpty 隊列中等待
private final Condition notEmpty = takeLock.newCondition();
// put,offer等插入操作的鎖
private final ReentrantLock putLock = new ReentrantLock();
// 數組滿而無法執行插入操作的線程被放入到 notFull 隊列中等待
private final Condition notFull = putLock.newCondition();
構造函數
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE); // 默認鏈最大長度爲Integer.MAX_VALUE
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
public LinkedBlockingQueue(Collection<? extends E> c) {
......
插入操作
add,offer
下面你會經常看見打了雙引號的 喚醒 兩字——“喚醒”:經過一開始的分析,AQS有個同步隊列,通過它得到的Condition對象也有一個等待隊列,await 就是線程放入等待隊列,signal 就是將等待隊列中的頭節點放回同步隊列尾,並非是喚醒線程,它在同步隊列中等待直到被喚醒。具體的分析在我的 Condition 文章中,鏈接在上面。
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity) // 數組滿直接返回false
return false;
int c = -1; // 開始時設爲-1,若插入成功c的值應>=0,以此來判定offer操作是否成功
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
// 若數組滿不做任何處理,c仍未-1,最後會返回false。
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement(); // 這裏返回的是增加之前的值
if (c + 1 < capacity) // 插入後數組未滿
notFull.signal(); // “喚醒”一個插入線程
}
} finally {
putLock.unlock();
}
// c等於0 說明本次插入之前數組爲空,則可鞥有不少獲取操作的線程都在阻塞等待,
// 所以可以在這裏喚醒一個,其實並不一定會喚醒線程,很可能是將節點從
// notEmpty 等待對隊列中放回 takeLock 的同步隊列。
// 具體分析見我分析 Condition 的文章
if (c == 0)
signalNotEmpty();
return c >= 0;
}
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
從上面可以看出這種雙鎖設計的好處,當前插入線程完成後“喚醒”下一個插入線程,跟取操作互不影響。代碼最後在檢測到此次插入前數組爲空的情況時,會“喚醒”一個取線程,防止 notEmpty 隊列中等待的取線程一直阻塞不被喚醒,當然無論是取還是插入,當其執行完後都會在”喚醒“下一個取或插入。
put
操作與上面基本相同,只是當數組滿時 阻塞 線程。
......
while (count.get() == capacity) { //數組滿就阻塞
notFull.await();
}
......
刪除操作
remove,poll
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0) // 數組爲空 返回null
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;
}
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;
E x = first.item;
//將新頭節點的item置空,已經刪除沒必要再持有對其的引用,不利於回收
first.item = null;
return x;
}
take 的實現與上面沒什麼不同,只是再數組爲空時阻塞。
獲取操作
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E peek() {
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();
}
}
實現很簡單。由於併發下 head 不斷變換,所以需要獲取取鎖以保證安全性。