目錄
3 ArrayBlockingQueue和LinkedBlockingQueue的區別
Java中的阻塞隊列
阻塞隊列(BlockingQueue
)是一個支持以下兩個附加操作的隊列:
- 支持阻塞的插入方法:當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。
- 支持阻塞的移除方法:在隊列爲空時,獲取元素的線程會等待隊列變爲非空。
阻塞隊列常用於生產者和消費者的場景,生產者是向隊列裏添加元素的線程,消費者是從隊列裏取元素的線程。阻塞隊列就是生產者用來存放元素、消費者用來獲取元素的容器。
在阻塞隊列不可用時,這兩個附加操作提供了以下4種處理方式:
方法/處理方式 | 拋出異常 | 返回特殊值 | 一直阻塞 | 超時退出 |
---|---|---|---|---|
插入方法 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
移除方法 | remove() |
poll() |
take() |
poll(time, unit) |
檢查方法 | element() |
peek() |
不可用 | 不可用 |
- 拋出異常:隊列滿時,再添加元素,會拋出
IllegalStateException("Queue full")
異常;當隊列爲空時,從隊列裏獲取元素會拋出NoSuchElementException
異常。 - 返回特殊值:往隊列插入元素時,返回
ture
表示插入成功。從隊列裏移除元素,即取出元素,如果沒有則返回null
。 - 一直阻塞:當阻塞隊列滿時,如果生產者線程往隊列裏
put
元素,隊列會一直阻塞生產者線程,直到隊列可用或者響應中斷退出。當隊列空時,如果消費者線程從隊列裏take
元素,隊列會阻塞住消費者線程,直到隊列不爲空。 - 超時退出:當阻塞隊列滿時,如果生產者線程往隊列裏插入元素,隊列會阻塞生產者線程一段時間,如果超過了指定的時間
time
,生產者線程就會退出。
如果是無界阻塞隊列,隊列不可能會出現滿的情況,所以使用
put
或offer
方法永遠不會被阻塞,而且使用offer
方法時,該方法永遠返回true
。
1 ArrayBlockingQueue
ArrayBlockingQueue是一個有界隊列,採用數組存儲數據,遵循FIFO原則,隊列的頭元素是隊列中存在時間最長的元素,尾結點是存在時間最短的元素。ArrayBlockingQueue在創建時指定容量,一旦創建不可更改。當隊列滿時,入隊操作線程會阻塞;當隊列空時,出隊操作線程會阻塞。ArrayBlockingQueue也支持公平和非公平的入隊和出隊,通過ReentrantLock的公平機制來實現。
1.1 成員變量
使用ReentrantLock來對生產和消費動作進行加鎖,使它們互斥執行。同時使用ReentrantLock的兩個Condition對生產者和消費者線程進行等待/喚醒。
// 數據容器
final Object[] items;
// 要消費的下一個元素的索引
int takeIndex;
// 要生產的下一個位置的索引
int putIndex;
// 隊列中元素個數
int count;
// 控制生產和消費互斥的獨佔鎖
final ReentrantLock lock;
// 用來對消費者進行等待/喚醒操作的對象
private final Condition notEmpty;
// 用來對生產者進行等待/喚醒操作的對象
private final Condition notFull;
1.2 構造方法
構造ArrayBlockingQueue時必須要指定容量capacity,即數組items的長度,一旦創建,不能修改容量。可以傳入boolean參數來指定上面ReentrantLock的鎖的類型(true爲公平鎖,false爲非公平鎖),默認是非公平鎖。可以傳入一個集合,對items進行數據初始化插入。
public ArrayBlockingQueue(int capacity, boolean fair,Collection<? extends E> c) {
......
}
1.3 put方法
就是加鎖,然後將元素插入到items中。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) // 隊列滿,線程等待在notFull上
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0; // 回到items[0],循環
count++;
notEmpty.signal(); // 喚醒一個等待在notEmpty上的消費者線程
}
1.4 take方法
就是加鎖,然後將元素從items中出隊。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await(); // 隊列滿,線程等待在notEmpty上
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal(); // 喚醒一個等待在notFull上的生產者線程
return x;
}
2 LinkedBlockingQueue
LinkedBlockingQueue是一個可選擇長度的有界隊列,採用鏈表存儲數據,不指定長度就是Integer.MAX_VALUE,遵循FIFO原則,隊列的頭元素是隊列中存在時間最長的元素,尾結點是存在時間最短的元素。當隊列滿時,入隊操作線程會阻塞;當隊列空時,出隊操作線程會阻塞。LinkedBlockingQueue使用兩把鎖分別對生產和消費操作進行同步。相較於ArrayBlockingQueue,LinkedBlockingQueue有更高的吞吐量,但在大部分應用中有更多的不確定性。(jdk原文)
2.1 成員變量
與ArrayBlockingQueue的不同之處在於,LinkedBlockingQueue使用了兩把鎖分別控制消費和生產。注意count是AtomicInteger類型的。
static class Node<E> { // 鏈表節點
E item;
// 若next等於自己,表示該node是head
// 若next等於null,表示該node是tail
// 否則next表示該node的後繼節點
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;
// 消費鎖
private final ReentrantLock takeLock = new ReentrantLock();
// 用來對消費者進行等待/喚醒操作的對象
private final Condition notEmpty = takeLock.newCondition();
// 生產鎖
private final ReentrantLock putLock = new ReentrantLock();
// 用來對生產者進行等待/喚醒操作的對象
private final Condition notFull = putLock.newCondition();
2.2 構造方法
可以指定鏈表長度,指定長度就是Integer.MAX_VALUE。顯然,LinkedBlockingQueue默認採用非公平鎖。
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
// 將c中數據插入鏈表中
}
2.3 put方法
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) { // 判斷容量是否滿了
notFull.await(); // 當前線程等待在notFull上
}
enqueue(node);
c = count.getAndIncrement(); // 原子更新count,一直阻塞在此,直到更新成功
if (c + 1 < capacity)
notFull.signal(); // 喚醒下一個生產者線程
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty(); // 若插入後count爲1,則喚醒一個消費者
}
2.4 take方法
take方法與put方法基本一樣,只是判斷條件和操作不一樣。
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
3 ArrayBlockingQueue和LinkedBlockingQueue的區別
- ArrayBlockingQueue中生產和消費都是使用同一個鎖;LinkedBlockingQueue中使用兩個鎖,生產使用putLock,消費使用takeLock。
- ArrayBlockingQueue生產時直接將數據插入到數組中;LinkedBlockingQueue則要先將數據包裝成Node,再插入鏈表中,多創建了一個對象。
- ArrayBlockingQueue在創建的時候長度必須制定,且創建後不能修改;LinkedBlockingQueue在創建時可以指定長度,也可不指定,不指定長度爲Integer.MAX_VALUE。