簡介
多執行緒環境中,通過佇列可以很容易實現資料共享,比如經典的“生產者”和“消費者”模型中,通過佇列可以很便利地實現兩者之間的資料共享。假設我們有若干生產者執行緒,另外又有若干個消費者執行緒。如果生產者執行緒需要把準備好的資料共享給消費者執行緒,利用佇列的方式來傳遞資料,就可以很方便地解決他們之間的資料共享問題。但如果生產者和消費者在某個時間段內,萬一發生資料處理速度不匹配的情況呢?理想情況下,如果生產者產出資料的速度大於消費者消費的速度,並且當生產出來的資料累積到一定程度的時候,那麼生產者必須暫停等待一下(阻塞生產者執行緒),以便等待消費者執行緒把累積的資料處理完畢,反之亦然。
BlockingQueue很好地解決了上述問題,BlockingQueue即阻塞佇列,它是一個介面,它的實現類有ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,它們的區別主要體現在儲存結構上或對元素操作上的不同。
常用方法
public interface BlockingQueue<E> extends Queue<E> {
//往佇列尾部新增元素,如果BlockingQueue可以容納,則返回true,否則丟擲異常
boolean add(E e);
//移除元素,如果有這個元素則就回true,否則丟擲異常
boolean remove(Object o);
//往佇列尾部新增元素,如果BlockingQueue可以容納則返回true,否則返回false.
//如果是往限定了長度的佇列中設定值,推薦使用offer()方法。
boolean offer(E e);
//和上面的方法差不多,不過如果佇列滿了可以阻塞等待一段時間
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
//取出頭部物件,若不能立即取出,則可以等time引數規定的時間,取不到時返回null
E poll(long timeout, TimeUnit unit) throws InterruptedException;
//往佇列尾部新增元素,如果沒有空間,則呼叫此方法的執行緒被阻塞直到有空間再繼續.
void put(E e) throws InterruptedException;
//取出頭部物件,若BlockingQueue為空,阻斷進入等待狀態直到Blocking有新的物件被加入為止
E take() throws InterruptedException;
//剩餘容量,超出此容量,便無法無阻塞地新增元素
int remainingCapacity();
//判斷佇列中是否擁有該值。
boolean contains(Object o);
//一次性從BlockingQueue獲取所有可用的資料物件,可以提升獲取資料效率
int drainTo(Collection<? super E> c);
//和上面的方法差不多,不過限制了最大取出數量
int drainTo(Collection<? super E> c, int maxElements);
}
複製程式碼
原始碼簡析
我們以ArrayBlockingQueue
為例分析下上述方法:
offer(E 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();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal();
}
複製程式碼
offer操作如上,程式碼比較簡單,可見阻塞佇列是通過可重入保證執行緒安全。enqueue
方法也說明了ArrayBlockingQueue
是通過陣列的形式儲存資料的。如果佇列滿了直接會返回false,不會阻塞執行緒。
put(E 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方法在佇列已滿的情況下會阻塞執行緒,take、poll等方法會呼叫dequeue方法出列,從而呼叫notFull.signal(),從而喚醒阻塞在put方法中執行緒去繼續進行入列操作:
take()
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
複製程式碼
poll(long timeout, TimeUnit unit)
從對頭取出一個元素:如果陣列不空,出隊;如果陣列已空且已經超時,返回null;如果陣列已空則進入等待,直到被喚醒或超時:
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;
//阻塞指定時間,enqueue()方法會呼叫notEmpty.signal()喚醒進行poll操作的執行緒
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
複製程式碼
參考文章和擴充套件閱讀
雖然只講了阻塞佇列,但涉及了ReentrantLock、中斷、Condition等知識點,如果不清楚的話可以看下下面的幾篇文章: