目錄
1.概述
- ArrayBlockingQueue 是 BlockingQueue 接口的有界阻塞隊列實現類,底層採用數組來實現。
特點
- ArrayBlockingQueue一旦創建,容量不能改變。
- 其併發控制採用可重入鎖來控制,不管是插入操作還是讀取操作,都需要獲取到鎖才能進行操作。
- ArrayBlockingQueue 默認情況下不能保證線程訪問隊列的公平性,
- 所謂公平性是指嚴格按照線程等待的絕對時間順序,即最先等待的線程能夠最先訪問到 ArrayBlockingQueue。
- 而非公平性則是指訪問 ArrayBlockingQueue 的順序不是遵守嚴格的時間順序,有可能存在,
當 ArrayBlockingQueue 可以被訪問時,長時間阻塞的線程依然無法訪問到 ArrayBlockingQueue。如果保證公平性,通常會降低吞吐量。如果需要獲得公平性的 ArrayBlockingQueue,可採用如下代碼:
private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
2.使用案例
使用案例見上一篇博客:https://blog.csdn.net/qq_34805255/article/details/101924873
3.源碼分析
3.1 重要屬性
public class ArrayBlockingQueue {
/**
* ArrayBlockingQueue的底層實現爲此數組
*
* 並且可以看到此數組被final修飾,數組一旦創建,容量不可變(數組本身的性質),並且,數組引用指向的數組對象不可變
*/
final Object[] items;
/**
* 隊首索引位置
*/
int takeIndex;
/**
* 隊尾索引位置
*/
int putIndex;
/**
* 隊列中元素的個數
*/
int count;
/**
* 採用ReentrantLock來保證線程安全
*/
final ReentrantLock lock;
/**
* 爲了保證消費(take)數據的時候如果爲空的時候,進行阻塞(等待),使用了Condition
*
* 當獲取數據的消費者線程被阻塞時會將該線程放置到notEmpty等待隊列中(notEmpty可以理解爲等待隊列不空的等待隊列)
*/
private final Condition notEmpty;
/**
* 爲了保證生產(put)數據的時候如果隊列滿的時候,進行阻塞(等待),使用了Condition
*
* 當插入數據的生產者線程被阻塞時,會將該線程放置到notFull等待隊列中(notFull可以理解爲等待隊列不滿的等待隊列)。
*/
private final Condition notFull;
}
- 底層採用final數組items實現
- 使用鎖ReentrantLock保證線程安全
- 使用兩個Condition來實現阻塞
3.2 構造方法
/**
* 我們在創建一個ArrayBlockingQueue時,必須傳入容量用做創建數組的大小
*
* 當我們不傳入是否使用公平鎖時,默認是非公平的
*/
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();
}
- 構造函數就是初始化了一些屬性,如數組,鎖,兩個條件隊列
3.3 私有方法入隊與出隊
(1)入隊
/**
* 讓元素入隊
*/
private void enqueue(E x) {
final Object[] items = this.items;
//插入數據到隊尾
items[putIndex] = x;
//如果隊尾剛好是數組的最後一個位置,那麼,插入後隊尾位置變成第一個位置
if (++putIndex == items.length)
putIndex = 0;
//插入後,元素數量加1
count++;
//通知被阻塞的消費者線程
notEmpty.signal();
}
- 通過上述的可以發現:
- ArrayBlockingQueue的底層是使用數組實現了一個環形隊列,使用takeIndex標誌隊列的隊首位置,使用putIndex標誌隊列的隊尾位置
- 當隊尾到達數組的最後一個位置時,插入後的下一個隊尾位置又會回到開頭
- 它是如何判斷隊列滿的呢?
- 我們在put和offer方法中都看到,即爲判斷隊列滿的語句,只有這個條件不滿足,即隊列不滿,纔會調用私有方法enqueue
(2)出隊
/**
* 讓元素出隊
*/
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//先用臨時變量保存隊首元素
E x = (E) items[takeIndex];
//然後將隊首引用置空
items[takeIndex] = null;
////如果隊首剛好是數組的最後一個位置,那麼,刪除元素後隊首位置變成第一個位置
if (++takeIndex == items.length)
takeIndex = 0;
//隊首元素出隊後,元素數量減1
count--;
if (itrs != null)
itrs.elementDequeued();
//通知被阻塞的生產者線程
notFull.signal();
return x;
}
dequeue方法也主要做了兩件事情:
- 1. 獲取隊列中的數據,即獲取數組中的數據元素(
(E) items[takeIndex]
),刪除隊首元素的實現是通過讓原來隊首引用指向null,然後讓該引用原來指向的堆中的元素被GC回收 - 2. 通知notFull等待隊列中的線程,使其由等待隊列移入到同步隊列中,使其能夠有機會獲得lock,並執行完成功退出。
3.4 put和take方法
(1)put
/**
* 阻塞插入:
* 即插入時,在隊列滿的時候阻塞,在隊列不滿的時候直接插入
*/
public void put(E e) throws InterruptedException {
//檢查傳入的元素不爲空
checkNotNull(e);
final ReentrantLock lock = this.lock;
//加可中斷鎖
lock.lockInterruptibly();
try {
//當隊列滿的時候,阻塞
while (count == items.length)
notFull.await();
//當隊列不滿的時候,入隊
enqueue(e);
} finally {
//釋放鎖
lock.unlock();
}
}
(2)take
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//當隊列空的時候,阻塞
while (count == 0)
notEmpty.await();
//當隊列不空的時候,直接返回隊首元素
return dequeue();
} finally {
lock.unlock();
}
}
3.5 offer和poll
(1)offer
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//與put不同的是加的是lock
lock.lock();
try {
//隊列滿的時候,直接返回false,表示插入不成功
if (count == items.length)
return false;
//隊列不滿的時候,入隊插入元素,返回true,表示插入成功
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
(2)poll
public E poll() {
final ReentrantLock lock = this.lock;
//使用lock保證線程安全
lock.lock();
try {
//隊列爲空,返回null,否則出隊隊首元素,並返回
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
3.6 peek
public E peek() {
final ReentrantLock lock = this.lock;
//通過lock保證線程安全
lock.lock();
try {
//當隊列爲空的時候返回null,否則返回隊尾元素
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
/**
* 當隊列爲空的時候,該索引位置的值爲null,所以會返回null
*/
final E itemAt(int i) {
return (E) items[i];
}
3.7 remainingCapacity
/**
* 計算剩餘容量
*/
public int remainingCapacity() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//加鎖計算剩餘的容量
return items.length - count;
} finally {
lock.unlock();
}
}
3.8 remove
/**
* 移除指定的元素
*/
public boolean remove(Object o) {
//對輸入元素值做檢查
if (o == null) return false;
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
//加鎖保證線程安全
lock.lock();
try {
//如果隊列不爲空
if (count > 0) {
final int putIndex = this.putIndex;
int i = takeIndex;
//從隊首開始遍歷隊列
do {
//如果傳入的元素值和該索引位置的該元素相等,則移除該元素返回true
if (o.equals(items[i])) {
removeAt(i);
return true;
}
//在遍歷過程中,遍歷指針i到最後一個位置時,它的下一個位置迭代爲0,否則++就行
if (++i == items.length)
i = 0;
} while (i != putIndex);
}
return false;
} finally {
lock.unlock();
}
}
4.總結
- 可以發現ArrayBlockingQueue基本上所有的public方法都使用獨佔鎖ReentrantLock的對象lock進行加鎖來保證線程安全
- 僅僅在put和take方法的時候加的是可中斷獨佔鎖
- 使用兩個Condition的等待(await)-通知(signalAll)機制來實現阻塞,使用數組實現環形隊列
-
對於 ArrayBlockingQueue,我們可以在構造的時候指定以下三個參數:
- 隊列容量,其限制了隊列中最多允許的元素個數,即數組的大小
- 指定獨佔鎖是公平鎖還是非公平鎖。非公平鎖的吞吐量比較高,公平鎖可以保證每次都是等待最久的線程獲取到鎖;
- 可以指定用一個集合來初始化,將此集合中的元素在構造方法期間就先添加到隊列中。