ArrayBlockingQueue是Java併發框架中阻塞隊列的最基本的實現,分析這個類就可以知道併發框架中是如何實現阻塞的。它是數組實現的線程安全的有界阻塞隊列。線程安全是指,ArrayBlockingQueue內部通過“互斥鎖”保護競爭資源,實現了多線程對競爭資源的互斥訪問。而有界,則是指ArrayBlockingQueue對應的數組是有界限的。 阻塞隊列,是指多線程訪問競爭資源時,當競爭資源已被某線程獲取時,其它要獲取該資源的線程需要阻塞等待,ArrayBlockingQueue實現阻塞隊列的關鍵在與,對鎖(Lock)和等待條件(Condition)的使用,這兩個實現的基本功能類似域wait()和notify(),是wait()和notify()的高級用法;而且,ArrayBlockingQueue是按 FIFO(先進先出)原則對元素進行排序,元素都是從尾部插入到隊列,從頭部開始返回。
注意:ArrayBlockingQueue不同於ConcurrentLinkedQueue,ArrayBlockingQueue是數組實現的,並且是有界限的;而ConcurrentLinkedQueue是鏈表實現的,是無界限的。
1.ArrayBlockingQueue原理和數據結構
ArrayBlockingQueue的數據結構,如下圖所示:
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {} //繼承體系 java.lang.Object java.util.AbstractCollection<E> java.util.AbstractQueue<E> java.util.concurrent.ArrayBlockingQueue<E> //實現的所有接口: Serializable, Iterable<E>, Collection<E>, BlockingQueue<E>, Queue<E>
//在前面我們學習與synchronized鎖配合的線程等待(Object.wait)與線程通知(Object.notify), //那麼對於JDK1.5 的 java.util.concurrent.locks.ReentrantLock 鎖,JDK也爲我們提供了與此功能相 //應的類java.util.concurrent.locks.Condition。Condition與重入鎖是通過lock.newCondition()方 //法產生一個與當前重入鎖綁定的Condtion實例,我們通知該實例來控制線程的等待與通知 public interface Condition { //使當前線程加入 await() 等待隊列中,並釋放當鎖,當其他線程調用signal()會重新請求鎖。與Object.wait()類似。 void await() throws InterruptedException; //調用該方法的前提是,當前線程已經成功獲得與該條件對象綁定的重入鎖,否則調用該方法時會拋出IllegalMonitorStateException。 //調用該方法後,結束等待的唯一方法是其它線程調用該條件對象的signal()或signalALL()方法。等待過程中如果當前線程被中斷,該方法仍然會繼續等待,同時保留該線程的中斷狀態。 void awaitUninterruptibly(); // 調用該方法的前提是,當前線程已經成功獲得與該條件對象綁定的重入鎖,否則調用該方法時會拋出IllegalMonitorStateException。 //nanosTimeout指定該方法等待信號的的最大時間(單位爲納秒)。若指定時間內收到signal()或signalALL()則返回nanosTimeout減去已經等待的時間; //若指定時間內有其它線程中斷該線程,則拋出InterruptedException並清除當前線程的打斷狀態;若指定時間內未收到通知,則返回0或負數。 long awaitNanos(long nanosTimeout) throws InterruptedException; //與await()基本一致,唯一不同點在於,指定時間之內沒有收到signal()或signalALL()信號或者線程中斷時該方法會返回false;其它情況返回true。 boolean await(long time, TimeUnit unit) throws InterruptedException; //適用條件與行爲與awaitNanos(long nanosTimeout)完全一樣,唯一不同點在於它不是等待指定時間,而是等待由參數指定的某一時刻。 boolean awaitUntil(Date deadline) throws InterruptedException; //喚醒一個在 await()等待隊列中的線程。與Object.notify()相似 void signal(); //喚醒 await()等待隊列中所有的線程。與object.notifyAll()相似 void signalAll(); }
說明:
1. ArrayBlockingQueue繼承於AbstractQueue,並且它實現了BlockingQueue接口。
2. ArrayBlockingQueue內部是通過Object[]數組保存數據的,也就是說ArrayBlockingQueue本質上是通過數組實現的。ArrayBlockingQueue的大小,即數組的容量是創建ArrayBlockingQueue時指定的。
3. ArrayBlockingQueue與ReentrantLock是組合關係,ArrayBlockingQueue中包含一個ReentrantLock對象(lock)。ReentrantLock是可重入的互斥鎖,ArrayBlockingQueue就是根據該互斥鎖實現“多線程對競爭資源的互斥訪問”。而且,ReentrantLock分爲公平鎖和非公平鎖,關於具體使用公平鎖還是非公平鎖,在創建ArrayBlockingQueue時可以指定;而且,ArrayBlockingQueue默認會使用非公平鎖。
4. ArrayBlockingQueue與Condition是組合關係,ArrayBlockingQueue中包含兩個Condition對象(notEmpty和notFull)。而且,Condition又依賴於ArrayBlockingQueue而存在,通過Condition可以實現對ArrayBlockingQueue的更精確的訪問 :(01)若某線程(線程A)要取數據時,數組正好爲空,則該線程會執行notEmpty.await()進行等待;當其它某個線程(線程B)向數組中插入了數據之後,會調用notEmpty.signal()喚醒“notEmpty上的等待線程”。此時,線程A會被喚醒從而得以繼續運行。
(02)若某線程(線程H)要插入數據時,數組已滿,則該線程會它執行notFull.await()進行等待;當其它某個線程(線程I)取出數據之後,會調用notFull.signal()喚醒“notFull上的等待線程”。此時,線程H就會被喚醒從而得以繼續運行。
5.在調用await()方法前線程必須獲得重入鎖,調用await()方法後線程會釋放當前佔用的鎖。同理在調用signal()方法時當前線程也必須獲得相應重入鎖,調用signal()方法後系統會從condition.await()等待隊列中喚醒一個線程。當線程被喚醒後,它就會嘗試重新獲得與之綁定的重入鎖,一旦獲取成功將繼續執行。所以調用signal()方法後一定要釋放當前佔用的鎖,這樣被喚醒的線程纔能有獲得鎖的機會,才能繼續執行。
2.源碼分析
2.1主要屬性
/** The queued items ,fianl修飾,比volatile更嚴格*/ final Object[] items; /** items index for next take, poll, peek or remove ,取下一個元素的指針*/ int takeIndex; /** items index for next put, offer, or add ,放下一個元素的指針*/ int putIndex; /** Number of elements in the queue ,元素數量*/ int count; /* * Concurrency control uses the classic two-condition algorithm * found in any textbook. */ /** Main lock guarding all access ,保證併發訪問的可重入鎖*/ final ReentrantLock lock; /** Condition for waiting takes */ private final Condition notEmpty; /** Condition for waiting puts */ private final Condition notFull;
2.2主要構造方法
/** * Creates an {@code ArrayBlockingQueue} with the given (fixed) * capacity and default access policy(非公平). * * @param capacity the capacity of this queue * @throws IllegalArgumentException if {@code capacity < 1} */ public ArrayBlockingQueue(int capacity) { this(capacity, false); //非公平鎖 } /** * Creates an {@code ArrayBlockingQueue} with the given (fixed) * capacity and the specified access policy. * * @param capacity the capacity of this queue * @param fair if {@code true} then queue accesses for threads blocked * on insertion or removal, are processed in FIFO order; * if {@code false} the access order is unspecified. * @throws IllegalArgumentException if {@code capacity < 1} */ public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0) throw new IllegalArgumentException(); this.items = new Object[capacity]; //fair是“可重入的獨佔鎖(ReentrantLock)”的類型。fair爲true,表示是公平鎖;fair爲false,表示是非公平鎖。在公平的鎖中,如果有另一個線程持有鎖或者有其他線程在等待隊列中等待這個所,那麼新發出的請求的線程將被放入到隊列中。而非公平鎖上,只有當鎖被某個線程持有時,新發出請求的線程纔會被放入隊列中(此時和公平鎖是一樣的)。所以,它們的差別在於非公平鎖會有更多的機會去搶佔鎖。 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(); // Lock only for visibility, not mutual exclusion 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; //putIndex表示當前有多少個數據,takeIndex表示從隊列拿出來的數量 } finally { lock.unlock(); } }
Lock的作用是提供獨佔鎖機制,來保護競爭資源;而Condition是爲了更加精細的對鎖進行控制,它依賴於Lock,通過某個條件對多線程進行控制。
2.3入隊
入隊有四個方法,它們分別是add(E e)、offer(E e)、put(E e)、offer(E e, long timeout, TimeUnit unit),它們有什麼區別呢?
public boolean add(E e) { return super.add(e); } //offer方法是立刻返回的,它並不像其他方法那樣,當隊列滿時會一直等待。 public boolean offer(E e) { Objects.requireNonNull(e); final ReentrantLock lock = this.lock; lock.lock(); try { if (count == items.length) //這裏count是不是有某種限制,使得不能超過items.length return false; //立即返回 else { enqueue(e); return true; } } finally { lock.unlock(); } } /** * Inserts element at current put position, advances, and signals. * Call only when holding lock. */ private void enqueue(E e) { // assert lock.isHeldByCurrentThread(); // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = e; // 如果放指針到數組盡頭了,就返回頭部 if (++putIndex == items.length) putIndex = 0; // 元素數量加1 count++; // 喚醒notEmpty,因爲入隊了一個元素,所以肯定不爲空了 notEmpty.signal(); } /** * Inserts the specified element at the tail of this queue, waiting * up to the specified wait time for space to become available if * the queue is full. * * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ 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 { // 如果數組滿了,就阻塞nanos納秒 // 如果喚醒這個線程時依然沒有空間且時間到了就返回false //這裏之所以使用while而不是if是因爲有可能多個線程阻塞在lock上,即使喚醒了可能其它線程先一步修改了隊列又變成滿的了,這時候需要再次等待或者納秒數耗盡直接退出 while (count == items.length) { if (nanos <= 0L) return false; nanos = notFull.awaitNanos(nanos); } enqueue(e); return true; } finally { lock.unlock(); } } /** * Inserts the specified element at the tail of this queue, waiting * for space to become available if the queue is full. * * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public void put(E e) throws InterruptedException { Objects.requireNonNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { // 如果數組滿了,使用notFull等待 // notFull等待的意思是說現在隊列滿了 // 只有取走一個元素後,隊列纔不滿 // 然後喚醒notFull,然後繼續現在的邏輯 // 這裏之所以使用while而不是if // 是因爲有可能多個線程阻塞在lock上 // 即使喚醒了可能其它線程先一步修改了隊列又變成滿的了 // 這時候需要再次等待 while (count == items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } }
(1)add(e)時如果隊列滿了則拋出異常;
(2)offer(e)時如果隊列滿了則返回false;
(3)put(e)時如果隊列滿了則使用notFull等待;
(4)offer(e, timeout, unit)時如果隊列滿了則等待一段時間後如果隊列依然滿就返回false;
(5)利用指針循環使用數組來存儲元素;
出隊
出隊有四個方法,它們分別是remove()、poll()、take()、poll(long timeout, TimeUnit unit),它們有什麼區別呢?
//刪除基於循環數組的隊列中的內部元素本質上是一個緩慢且具有破壞性的操作,所以應該只在特殊情況下執行,
//理想情況下,只有當已知隊列不能被其他線程訪問時才執行。
public boolean remove(Object o) {
if (o == null) return false;
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count > 0) {
final Object[] items = this.items;
//本次查詢分兩種情況。第一種情況是takeIndex < putIndex,此時查詢終點是putIndex,
//第二種情況是takeIndex >= putIndex,此時查詢分兩段:takeIndex-item.length、0-putIndex,
//原因是該隊列是一個循環隊列,當putIndex到達數組容量上限時回返回到開頭
for (int i = takeIndex, end = putIndex,
to = (i < end) ? end : items.length;
; i = 0, to = end) {
for (; i < to; i++)
if (o.equals(items[i])) {
removeAt(i);
return true;
}
if (to == end) break;
}
}
return false;
} finally {
lock.unlock();
}
}
// 這個方法是不阻塞的,當隊列未空的時候,直接返回null值,所以實現中只是一個鎖的簡單使用,防止併發問題
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果隊列無元素,則阻塞等待nanos納秒
// 如果下一次這個線程獲得了鎖但隊列依然無元素且已超時就返回null
while (count == 0) {
if (nanos <= 0L)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue(); //添加與刪除的返回值不同,offer()返回布爾值,而poll()返回E或null
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
/**
* Extracts element at current take position, advances, and signals.
* Call only when holding lock.
*/
private E dequeue() {
// assert lock.isHeldByCurrentThread();
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
// 取取指針位置的元素,能這樣做的原因是外部調用dequeue()的take()與poll()都獲取了鎖,並且通過判斷知道數組中元素數量count不爲0
E e = (E) items[takeIndex];
// 把取指針位置設爲null
items[takeIndex] = null;
// 取指針後移,如果數組到頭了就返回數組前端循環利用
if (++takeIndex == items.length) takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued(); //取出元素可能使得該數組上的迭代器發生變化,所以同通知各個迭代器並作出相應動作
//喚醒notFull條件
notFull.signal();
return e;
}
void removeAt(final int removeIndex) {
// assert lock.isHeldByCurrentThread();
// assert lock.getHoldCount() == 1;
// assert items[removeIndex] != null;
// assert removeIndex >= 0 && removeIndex < items.length;
final Object[] items = this.items;
if (removeIndex == takeIndex) {
// removing front item; just advance
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
} else {
// an "interior" remove
// slide over all others up through putIndex.
for (int i = removeIndex, putIndex = this.putIndex;;) {
int pred = i;
if (++i == items.length) i = 0;
if (i == putIndex) {
items[pred] = null;
this.putIndex = pred;
break;
}
items[pred] = items[i];
}
count--;
if (itrs != null)
itrs.removedAt(removeIndex);
}
notFull.signal(); //喚醒添加線程
}
(1)remove()時如果隊列爲空則拋出異常;
(2)poll()時如果隊列爲空則返回null;
(3)take()時如果隊列爲空則阻塞等待在條件notEmpty上;
(4)poll(timeout, unit)時如果隊列爲空則阻塞等待一段時間後如果還爲空就返回null;
(5)利用取指針循環從數組中取元素
跟操作阻塞隊列代碼似乎看完了,還剩餘一些查詢的,基本是修改阻塞隊列的時候加上鎖,排除鎖的代碼就是在操作一個數組環形隊列,但我們可以在思考一些問題:
如何判斷隊列的大小?count ?
如何判斷當前將要出隊的元素 items[takeIndex]?
我之所以加上? 是因爲考慮我在判斷隊列大小或者出隊元素的時候其它線程有可能正在修改隊列這個時候會差生一下影響,看下JDK是如何實現的?
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
//返回此隊列在理想情況下(在沒有內存或資源約束的情況下)可以不阻塞地接受的附加元素的數量。
//這總是等於這個隊列的初始容量減去這個隊列的當前{@code size}.注意,您不能總是通過檢查
//{@coderemainingCapacity}來判斷插入元素的嘗試是否會成功,因爲可能是另一個線程將要插入或
//刪除一個元素。length表示數組的長度,是數組的屬性
public int remainingCapacity() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return items.length - count;
} finally {
lock.unlock();
}
}
public boolean contains(Object o) {
if (o == null) return false;
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count > 0) {
final Object[] items = this.items;
for (int i = takeIndex, end = putIndex,
to = (i < end) ? end : items.length;
; i = 0, to = end) {
for (; i < to; i++)
if (o.equals(items[i]))
return true;
if (to == end) break;
}
}
return false;
} finally {
lock.unlock();
}
}
總結
(1)ArrayBlockingQueue是有界的阻塞隊列,不接受null 。底層數據接口是數組,下標putIndex/takeIndex,構成一個環形FIFO隊列
(2)ArrayBlockingQueue利用takeIndex和putIndex循環利用數組;所有的增刪改查數組公用了一把鎖ReentrantLock和兩個條件,入隊和出隊數組下標和count變更都是靠這把鎖來維護安全的。
(3)ArrayBlockingQueue不需要擴容,因爲是初始化時指定容量,並循環利用數組;
(4)阻塞的場景:1獲取lock鎖,2進入和取出還要滿足condition 滿了或者空了都等待出隊和加入喚醒,ArrayBlockingQueue我們主要是put和take真正用到的阻塞方法(條件不滿足)。
(5)成員cout /putIndex、takeIndex是共享的,所以一些查詢方法size、peek、toString、方法也是加上鎖保證線程安全,但沒有了併發損失了性能。
(6)remove(Object obj) 返回了第一個equals的Object
(7)隊和出隊各定義了四組方法爲滿足不同的用途;
參考:
https://www.cnblogs.com/tong-yuan/p/ArrayBlockingQueue.html
https://www.cnblogs.com/kexianting/p/8550598.html
https://blog.csdn.net/mayongzhan_csdn/article/details/80888655