顧名思義,ArrayBlockingQueue是基於數組實現的有界阻塞隊列。該隊列對元素進行FIFO排序。隊列的首元素是在該隊列中駐留時間最長的元素。隊列的尾部是在該隊列中停留時間最短的元素。新的元素被插入到隊列的尾部,隊列檢索操作獲取隊列頭部的元素。
ArrayBlockingQueue是一個經典的“有界緩衝區(bounded buffer)”,其中內部包含了一個固定大小的數組,用於承載包含生產者插入的和消費者提取的元素。ArrayBlockingQueue的容量一旦創建,不可更改。試圖將一個元素放入一個滿隊列將導致操作阻塞;試圖從空隊列中取出一個元素也同樣會阻塞。
ArrayBlockingQueue支持排序的可選公平策略,用於等待生產者和消費者線程。默認情況下,不保證此順序。然而,一個由公平性設置爲true構造的隊列允許線程以FIFO順序訪問。公平性一般會降低吞吐量,但可以減少可變性,避免線程餓死。
ArrayBlockingQueue類及其迭代器實現了Collection和Iterator接口的所有可選方法。ArrayBlockingQueue是Java Collections Framework的一個成員。
1. ArrayBlockingQueue的聲明
ArrayBlockingQueue的接口和繼承關係如下
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
…
}
完整的接口繼承關係如下圖所示。
從上述代碼可以看出,ArrayBlockingQueue既實現了BlockingQueue<E>和java.io.Serializable接口,又繼承了java.util.AbstractQueue<E>。其中,AbstractQueue是Queue接口的抽象類,核心代碼如下。
package java.util;
public abstract class AbstractQueue<E>
extends AbstractCollection<E>
implements Queue<E> {
protected AbstractQueue() {
}
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public void clear() {
while (poll() != null)
;
}
public boolean addAll(Collection<? extends E> c) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
}
2. ArrayBlockingQueue的成員變量和構造函數
以下是ArrayBlockingQueue的構造函數和成員變量。
// 元素數組
final Object[] items;
// 消費索引,用於take、poll、peek或remove操作
int takeIndex;
// 生產索引,用於put、offer或add操作
int putIndex;
// 隊列中的元素個數
int count;
/*
* 使用經典的雙條件算法(two-condition algorithm)實現併發控制
*/
// 操作數組確保原子性的鎖
final ReentrantLock lock;
// 數組非空,喚醒消費者
private final Condition notEmpty;
// 數組非滿,喚醒生產者
private final Condition notFull;
// 迭代器狀態
transient Itrs itrs;
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // 只鎖可見,不互斥
try {
final Object[] items = this.items;
int i = 0;
try {
for (E e : c)
items[i++] = Objects.requireNonNull(e);
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock(); // 解鎖
}
}
從上述代碼可以看出,構造函數有三種。構造函數中的參數含義如下
l capacity用於設置隊列容量
l fair用於設置訪問策略,如果爲true,則對線程的訪問在插入或移除時被阻塞,則按FIFO順序處理;如果爲false,則訪問順序未指定
l c用於設置最初包含給定集合的元素,按集合迭代器的遍歷順序添加
類成員items是一個數組,用於存儲隊列中的元素。關鍵字final指明瞭,當ArrayBlockingQueue構造完成之後,通過new Object[capacity]的方式初始化items數組完成後,則後續items的容量將不再變化。
訪問策略是通過ReentrantLock來實現的。通過兩個加鎖條件notEmpty、notFull來實現併發控制。這是典型的雙條件算法(two-condition algorithm)。
ArrayBlockingQueue生產則增加putIndex,消費則增加takeIndex。
Itrs用於記錄當前活動迭代器的共享狀態,如果已知不存在任何迭代器,則爲null。 允許隊列操作更新迭代器狀態。迭代器狀態不是本節的重點,不再深入探討。
3. ArrayBlockingQueue的核心方法
以下對ArrayBlockingQueue常用核心方法的實現原理進行解釋。
3.1. offer(e)
執行offer(e)方法後有兩種結果
l 隊列未滿時,返回 true
l 隊列滿時,返回 false
ArrayBlockingQueue的offer (e)方法源碼如下:
public boolean offer(E e) {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lock(); // 加鎖
try {
if (count == items.length)
return false;
else {
enqueue(e); // 入隊
return true;
}
} finally {
lock.unlock(); // 解鎖
}
}
從上面代碼可以看出,執行offer(e)方法時,分爲以下幾個步驟:
l 爲了確保併發操作的安全先做了加鎖處理。
l 而後判斷count是否與數組items的長度一致,如果一致則證明隊列已經滿了,直接返回false;否則執行enqueue(e)方法做元素的入隊,並返回true。
l 最後解鎖。
enqueue(e)方法源碼如下:
private void enqueue(E e) {
final Object[] items = this.items;
items[putIndex] = e;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal(); // 喚醒等待中的線程
}
上面代碼比較簡單,在當前索引(putIndex)位置放置待入隊的元素,而後putIndex和count分別遞增,並通過signal()方法喚醒等待中的線程。其中一個注意點是,當putIndex 等於數組items長度時,putIndex置爲0。
思考:當putIndex 等於數組items長度時,putIndex爲什麼置爲0呢?
3.2. put(e)
執行put(e)方法後有兩種結果:
•
l 隊列未滿時,直接插入沒有返回值
l 隊列滿時,會阻塞等待,一直等到隊列未滿時再插入
ArrayBlockingQueue的put (e)方法源碼如下:
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 獲取鎖
try {
while (count == items.length)
notFull.await(); // 使線程等待
enqueue(e); // 入隊
} finally {
lock.unlock(); // 解鎖
}
}
從上面代碼可以看出,put(e)方法的實現,分爲以下幾個步驟:
l 先是要獲取鎖。
l 而後判斷count是否與數組items的長度一致,如果一致則證明隊列已經滿了,就等待;否則執行enqueue(e)方法做元素的入隊。
l 最後解鎖。
3.3. offer(e,time,unit)
offer(e,time,unit)方法與offer(e)方法不同之處在於,前者加入了等待機制。設定等待的時間,如果在指定時間內還不能往隊列中插入數據則返回false。執行offer(e,time,unit)方法有兩種結果:
•
l 隊列未滿時,返回 true
l 隊列滿時,會阻塞等待,如果在指定時間內還不能往隊列中插入數據則返回 false
ArrayBlockingQueue的put (e)方法源碼如下:
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
Objects.requireNonNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 獲取鎖
try {
while (count == items.length) {
if (nanos <= 0L)
return false;
nanos = notFull.awaitNanos(nanos); // 使線程等待指定的時間
}
enqueue(e);
return true;
} finally {
lock.unlock(); // 解鎖
}
}
從上面代碼可以看出,offer(e,time,unit)方法的實現,分爲以下幾個步驟:
l 先是要獲取鎖。
l 而後判斷count是否與數組items的長度一致,如果一致則證明隊列已經滿了,就等待;否則執行enqueue(e)方法做元素的入隊。
l 最後解鎖。
3.4. add(e)
執行add(e)方法後有兩種結果
l 隊列未滿時,返回 true
l 隊列滿時,則拋出異常
ArrayBlockingQueue的add(e)方法源碼如下:
public boolean add(E e) {
return super.add(e);
}
從上面代碼可以看出,add(e)方法的實現,直接是調用了父類AbstractQueue的add(e)方法。而AbstractQueue的add(e)方法源碼如下:
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
從上面代碼可以看出,add(e)方法又調用了offer(e)方法。offer(e)方法此處不再贅述。
3.5. poll ()
執行poll ()方法後有兩種結果:
l 隊列不爲空時,返回隊首值並移除
l 隊列爲空時,返回 null
ArrayBlockingQueue的poll()方法源碼如下:
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock(); // 加鎖
try {
return (count == 0) ? null : dequeue(); // 出隊
} finally {
lock.unlock(); // 解鎖
}
}
從上面代碼可以看出,執行poll()方法時,分爲以下幾個步驟:
l 爲了確保併發操作的安全先做了加鎖處理。
l 而後判斷count是否等於0,如果等於0則證明隊列爲空,直接返回null;否則執行dequeue()方法做元素的出隊。
l 最後解鎖。
dequeue()方法源碼如下:
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E e = (E) items[takeIndex];
items[takeIndex] = null; // 刪除數據
if (++takeIndex == items.length) takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal(); // 喚醒等待中的線程
return e;
}
上面代碼比較簡單,在當前索引(takeIndex)位置取出待出隊的元素並刪除隊列中的元素,而後takeIndex遞增count遞減,並通過signal()方法喚醒等待中的線程。其中一個注意點是,當takeIndex等於數組items長度時,takeIndex置爲0。
3.6. take()
執行take()方法後有兩種結果:
l 隊列不爲空時,返回隊首值並移除
l 隊列爲空時,會阻塞等待,一直等到隊列不爲空時再返回隊首值
ArrayBlockingQueue的take ()方法源碼如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 獲取鎖
try {
while (count == 0)
notEmpty.await(); // 使線程等待
return dequeue(); // 出隊
} finally {
lock.unlock(); // 解鎖
}
}
從上面代碼可以看出,執行take()方法時,分爲以下幾個步驟:
l 先是要獲取鎖。
l 而後判斷count是否等於0,如果等於0則證明隊列爲空,會阻塞等待;否則執行dequeue()方法做元素的出隊。
l 最後解鎖。
dequeue()方法此處不再贅述。
3.7. poll(time,unit)
poll(time,unit)方法與poll()方法不同之處在於,前者加入了等待機制。設定等待的時間,如果在指定時間內隊列還爲空,則返回null。執行poll(time,unit)方法後有兩種結果:
l 隊列不爲空時,返回隊首值並移除
l 隊列爲空時,會阻塞等待,如果在指定時間內隊列還爲空則返回 null
ArrayBlockingQueue的poll(time,unit)方法源碼如下:
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 獲取鎖
try {
while (count == 0) {
if (nanos <= 0L)
return null;
nanos = notEmpty.awaitNanos(nanos); // 使線程等待指定的時間
}
return dequeue(); // 出隊
} finally {
lock.unlock(); // 解鎖
}
}
從上面代碼可以看出,執行poll(time,unit)方法時,分爲以下幾個步驟:
l 先是要獲取鎖。
l 而後判斷count是否等於0,如果等於0則證明隊列爲空,會阻塞等待;否則執行dequeue()方法做元素的出隊。
l 最後解鎖。
dequeue()方法此處不再贅述。
3.8. remove()
執行remove()方法後有兩種結果:
l 隊列不爲空時,返回隊首值並移除
l 隊列爲空時,拋出異常
ArrayBlockingQueue的remove()方法其實是調用了父類AbstractQueue的remove()方法,源碼如下:
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
從上面代碼可以看出,remove()直接調用了poll()方法。如果poll()方法返回結果爲null,則拋出NoSuchElementException異常。
poll()方法此處不再贅述。
3.9. peek()
執行peek()方法後有兩種結果:
l 隊列不爲空時,返回隊首值但不移除
l 隊列爲空時,返回null
peek()方法源碼如下:
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock(); // 加鎖
try {
return itemAt(takeIndex); // 空則返回null
} finally {
lock.unlock(); // 解鎖
}
}
final E itemAt(int i) {
return (E) items[i];
}
從上面代碼可以看出,peek()方法比較簡單,直接就是獲取了數組裏面的索引爲takeIndex的元素。
3.10. element()
執行element()方法後有兩種結果:
l 隊列不爲空時,返回隊首值但不移除
l 隊列爲空時,拋出異常
element()方法其實是調用了父類AbstractQueue的element()方法,源碼如下:
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
從上面代碼可以看出,執行element()方法時,先是獲取peek()方法的結果,如果結果是null,則拋出NoSuchElementException異常。
4. ArrayBlockingQueue的單元測試
ArrayBlockingQueue的單元測試如下:
package com.waylau.java.demo.datastructure;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.NoSuchElementException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
/**
* ArrayBlockingQueue Tests
*
* @since 1.0.0 2020年5月3日
* @author <a href="https://waylau.com">Way Lau</a>
*/
class ArrayBlockingQueueTests {
void testOffer() {
// 初始化隊列
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
// 測試隊列未滿時,返回 true
boolean resultNotFull = queue.offer("Java");
assertTrue(resultNotFull);
// 測試隊列滿則,返回 false
queue.offer("C");
queue.offer("Python");
boolean resultFull = queue.offer("C++");
assertFalse(resultFull);
}
void testPut() throws InterruptedException {
// 初始化隊列
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
// 測試隊列未滿時,直接插入沒有返回值;
queue.put("Java");
// 測試隊列滿則, 會阻塞等待,一直等到隊列未滿時再插入。
queue.put("C");
queue.put("Python");
queue.put("C++"); // 阻塞等待
}
@Test
void testOfferTime() throws InterruptedException {
// 初始化隊列
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
// 測試隊列未滿時,返回 true
boolean resultNotFull = queue.offer("Java", 5, TimeUnit.SECONDS);
assertTrue(resultNotFull);
// 測試隊列滿則,返回 false
queue.offer("C");
queue.offer("Python");
boolean resultFull = queue.offer("C++", 5, TimeUnit.SECONDS); // 等5秒
assertFalse(resultFull);
}
@Test
void testAdd() {
// 初始化隊列
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
// 測試隊列未滿時,返回 true
boolean resultNotFull = queue.add("Java");
assertTrue(resultNotFull);
// 測試隊列滿則拋出異常
queue.add("C");
queue.add("Python");
Throwable excpetion = assertThrows(IllegalStateException.class, () -> {
queue.add("C++");// 拋異常
});
assertEquals("Queue full", excpetion.getMessage());
}
@Test
void testPoll() throws InterruptedException {
// 初始化隊列
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
// 測試隊列爲空時,返回 null
String resultEmpty = queue.poll();
assertNull(resultEmpty);
// 測試隊列不爲空時,返回隊首值並移除
queue.put("Java");
queue.put("C");
queue.put("Python");
String resultNotEmpty = queue.poll();
assertEquals("Java", resultNotEmpty);
}
@Test
void testTake() throws InterruptedException {
// 初始化隊列
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
// 測試隊列不爲空時,返回隊首值並移除
queue.put("Java");
queue.put("C");
queue.put("Python");
String resultNotEmpty = queue.take();
assertEquals("Java", resultNotEmpty);
// 測試隊列爲空時,會阻塞等待,一直等到隊列不爲空時再返回隊首值
queue.clear();
String resultEmpty = queue.take(); // 阻塞等待
assertNotNull(resultEmpty);
}
@Test
void testPollTime() throws InterruptedException {
// 初始化隊列
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
// 測試隊列不爲空時,返回隊首值並移除
queue.put("Java");
queue.put("C");
queue.put("Python");
String resultNotEmpty = queue.poll(5, TimeUnit.SECONDS);
assertEquals("Java", resultNotEmpty);
// 測試隊列爲空時,會阻塞等待,如果在指定時間內隊列還爲空則返回 null
queue.clear();
String resultEmpty = queue.poll(5, TimeUnit.SECONDS); // 等待5秒
assertNull(resultEmpty);
}
@Test
void testRemove() throws InterruptedException {
// 初始化隊列
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
// 測試隊列爲空時,拋出異常
Throwable excpetion = assertThrows(NoSuchElementException.class, () -> {
queue.remove();// 拋異常
});
assertEquals(null, excpetion.getMessage());
// 測試隊列不爲空時,返回隊首值並移除
queue.put("Java");
queue.put("C");
queue.put("Python");
String resultNotEmpty = queue.remove();
assertEquals("Java", resultNotEmpty);
}
@Test
void testPeek() throws InterruptedException {
// 初始化隊列
Queue<String> queue = new ArrayBlockingQueue<String>(3);
// 測試隊列不爲空時,返回隊首值並但不移除
queue.add("Java");
queue.add("C");
queue.add("Python");
String resultNotEmpty = queue.peek();
assertEquals("Java", resultNotEmpty);
resultNotEmpty = queue.peek();
assertEquals("Java", resultNotEmpty);
resultNotEmpty = queue.peek();
assertEquals("Java", resultNotEmpty);
// 測試隊列爲空時,返回null
queue.clear();
String resultEmpty = queue.peek();
assertNull(resultEmpty);
}
@Test
void testElement() throws InterruptedException {
// 初始化隊列
Queue<String> queue = new ArrayBlockingQueue<String>(3);
// 測試隊列不爲空時,返回隊首值並但不移除
queue.add("Java");
queue.add("C");
queue.add("Python");
String resultNotEmpty = queue.element();
assertEquals("Java", resultNotEmpty);
resultNotEmpty = queue.element();
assertEquals("Java", resultNotEmpty);
resultNotEmpty = queue.element();
assertEquals("Java", resultNotEmpty);
// 測試隊列爲空時,拋出異常
queue.clear();
Throwable excpetion = assertThrows(NoSuchElementException.class, () -> {
queue.element();// 拋異常
});
assertEquals(null, excpetion.getMessage());
}
}
5. ArrayBlockingQueue的應用案例
以下是一個生產者-消費者的示例。該示例模擬了1個生產者,2個消費者。當隊列滿時,則會阻塞生產者生產;當隊列空時,則會阻塞消費者消費。
package com.waylau.java.demo.datastructure;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* ArrayBlockingQueue Demo
*
* @since 1.0.0 2020年5月3日
* @author <a href="https://waylau.com">Way Lau</a>
*/
public class ArrayBlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
// 1個生產者
Producer p = new Producer(queue);
// 2個消費者
Consumer c1 = new Consumer("c1", queue);
Consumer c2 = new Consumer("c2", queue);
// 啓動線程
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
class Producer implements Runnable {
private final BlockingQueue<String> queue;
Producer(BlockingQueue<String> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
// 模擬耗時操作
Thread.sleep(1000L);
queue.put(produce());
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
String produce() {
String apple = "apple: " + System.currentTimeMillis();
System.out.println("produce " + apple);
return apple;
}
}
class Consumer implements Runnable {
private final BlockingQueue<String> queue;
private final String name;
Consumer(String name, BlockingQueue<String> queue) {
this.queue = queue;
this.name = name;
}
public void run() {
try {
while (true) {
// 模擬耗時操作
Thread.sleep(2000L);
consume(queue.take());
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
void consume(Object x) {
System.out.println(this.name + " consume " + x);
}
}
運行上述程序,輸出內容如下:
produce apple: 1590308383034
c2 consume apple: 1590308383034
produce apple: 1590308384034
c1 consume apple: 1590308384034
produce apple: 1590308385036
c2 consume apple: 1590308385036
produce apple: 1590308386036
c1 consume apple: 1590308386036
produce apple: 1590308387036
c2 consume apple: 1590308387036
produce apple: 1590308388036
c1 consume apple: 1590308388036
produce apple: 1590308389041
c2 consume apple: 1590308389041
produce apple: 1590308390041
c1 consume apple: 1590308390041
produce apple: 1590308391042
c2 consume apple: 1590308391042
produce apple: 1590308392042
c1 consume apple: 1590308392042
6. 參考引用
本系列歸檔至《Java數據結構及算法實戰》:https://github.com/waylau/java-data-structures-and-algorithms-in-action
《數據結構和算法基礎(Java語言實現)》(柳偉衛著,北京大學出版社出版):https://item.jd.com/13014179.html