阻塞佇列 BlockingQueue(二)--ArrayBlockingQueue與LinkedBlockingQueue
ArrayBlockingQueue
主要引數:
/** 儲存資料的陣列 */
final Object[] items;
/**獲取資料的索引,主要用於take,poll,peek,remove方法 */
int takeIndex;
/**新增資料的索引,主要用於 put, offer, or add 方法*/
int putIndex;
/** 佇列元素的個數 */
int count;
/** 控制並非訪問的鎖 */
final ReentrantLock lock;
/**notEmpty條件物件,用於通知take方法佇列已有元素,可執行獲取操作 */
private final Condition notEmpty;
/**notFull條件物件,用於通知put方法佇列未滿,可執行新增操作 */
private final Condition notFull;
/** 迭代器 */
transient Itrs itrs = null;
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);//初始化ReentrantLock重入鎖,出隊入隊擁有這同一個鎖
notEmpty = lock.newCondition;//初始化非空等待佇列
notFull = lock.newCondition;//初始化非滿等待佇列
}
public ArrayBlockingQueue(int capacity, boolean fair, Collecation<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
// 這個鎖的操作並不是為了互斥操作,而是保證其可見性。
// 假如執行緒1是例項化ArrayBlockingQueue物件,執行緒2是對例項化的ArrayBlockingQueue物件做入隊操作
// (當然要保證執行緒1和執行緒2的執行順序),如果不對它進行加鎖操作(加鎖會保證其可見性,也就是寫回主存)
// 執行緒1的集合有可能只存線上程1維護的快取中,並沒有寫回主存
// 執行緒2中例項化的ArrayBlockingQueue維護的快取以及主存中並沒有集合存在
// 此時就因為可見性造成資料不一致的情況,引發執行緒安全問題。
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
item[i++] = e;//將集合新增進陣列構成的佇列中
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;//佇列中的實際資料數量
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
ArrayBlockingQueue內部通過陣列物件items來儲存所有的資料並通過一個ReentrantLock來同時控制新增執行緒與移除執行緒的併發訪問。
notEmpty條件物件用於存放等待或喚醒呼叫take方法的執行緒,告訴他們佇列已有元素,可以執行獲取操作。
notFull條件物件是用於等待或喚醒呼叫put方法的執行緒,告訴它們,佇列未滿,可以執行新增元素的操作。
takeIndex代表的是下一個方法(take,poll,peek,remove)被呼叫時獲取陣列元素的索引。
putIndex則代表下一個方法(put, offer, or add)被呼叫時元素新增到陣列中的索引。
新增元素時阻塞
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public boolean offer(E e) {
checkNotNull(e);//檢查元素是否為null
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;
//通過putIndex索引對陣列進行賦值
items[putIndex] = x;
//索引自增,如果已是最後一個位置,重新設定 putIndex = 0;
if (++putIndex == items.length)
putIndex = 0;
count++;//佇列中元素數量加1
//喚醒呼叫take()方法的執行緒,執行元素獲取操作。
notEmpty.signal();
}
enqueue方法內部通過putIndex索引直接將元素新增到陣列items中。
這裡需要注意的是當putIndex索引大小等於陣列長度時,需要將putIndex重新設定為0。這是因為當前佇列執行元素獲取時總是從佇列頭部獲取,而新增元素是從佇列尾部新增。所以當佇列索引(從0開始)與陣列長度相等時,需要從陣列頭部開始新增。
//put方法,阻塞時可中斷
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();
}
}
put方法是一個阻塞的方法,如果佇列元素已滿,那麼當前執行緒將會被notFull條件物件掛起加到等待佇列中,直到佇列有空檔才會喚醒執行新增操作。
但如果佇列沒有滿,那麼就直接呼叫enqueue(e)方法將元素加入到陣列佇列中。
移除元素時阻塞
移除元素有這些方法:poll、remove、take
poll方法獲取並移除此佇列的頭元素,若佇列為空,則返回 null
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//判斷佇列是否為null,不為null執行dequeue()方法,否則返回null
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
//刪除佇列頭元素並返回
private E dequeue() {
//拿到當前陣列的資料
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//獲取要刪除的物件
E x = (E) items[takeIndex];
將陣列中takeIndex索引位置設定為null
items[takeIndex] = null;
//takeIndex索引加1並判斷是否與陣列長度相等,
//如果相等說明已到盡頭,恢復為0
if (++takeIndex == items.length)
takeIndex = 0;
count--;//佇列個數減1
if (itrs != null)
itrs.elementDequeued();//同時更新迭代器中的元素資料
//刪除了元素說明佇列有空位,喚醒notFull條件物件新增執行緒,執行新增操作
notFull.signal();
return x;
}
remove(Object o)方法的刪除過程相對複雜些,因為該方法並不是直接從佇列頭部刪除元素。
首先執行緒先獲取鎖,接著判斷佇列count>0,這點是保證併發情況下刪除操作安全執行。
接下來獲取下一個要新增源的索引putIndex以及takeIndex索引 ,作為後續迴圈的結束判斷,因為只要putIndex與takeIndex不相等就說明佇列沒有結束。
然後通過while迴圈找到要刪除的元素索引,執行removeAt(i)方法刪除。
在removeAt(i)方法中實際上做了兩件事:
- 判斷佇列頭部元素是否為刪除元素,如果是直接刪除,並喚醒新增執行緒。
- 如果要刪除的元素並不是佇列頭元素,那麼執行迴圈操作,從要刪除元素的索引removeIndex之後的元素都往前移動一個位置,那麼要刪除的元素就被removeIndex之後的元素替換,從而也就完成了刪除操作。
public boolean remove(Object o) {
if (o == null) return false;
//獲取陣列資料
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock();//加鎖
try {
//如果此時佇列不為null,這裡是為了防止併發情況
if (count > 0) {
//獲取下一個要新增元素時的索引
final int putIndex = this.putIndex;
//獲取當前要被刪除元素的索引
int i = takeIndex;
//執行迴圈查詢要刪除的元素
do {
//找到要刪除的元素
if (o.equals(items[i])) {
removeAt(i);//執行刪除
return true;//刪除成功返回true
}
//當前刪除索引執行加1後判斷是否與陣列長度相等
//若為true,說明索引已到陣列盡頭,將i設定為0
if (++i == items.length)
i = 0;
} while (i != putIndex);//繼承查詢
}
return false;
} finally {
lock.unlock();
}
}
//根據索引刪除元素,實際上是把刪除索引之後的元素往前移動一個位置
void removeAt(final int removeIndex) {
final Object[] items = this.items;
//先判斷要刪除的元素是否為當前佇列頭元素
if (removeIndex == takeIndex) {
//如果是直接刪除
items[takeIndex] = null;
//當前佇列頭元素加1並判斷是否與陣列長度相等,若為true設定為0
if (++takeIndex == items.length)
takeIndex = 0;
count--;//佇列元素減1
if (itrs != null)
itrs.elementDequeued();//更新迭代器中的資料
} else {
//如果要刪除的元素不在佇列頭部,
//那麼只需迴圈迭代把刪除元素後面的所有元素往前移動一個位置
//獲取下一個要被新增的元素的索引,作為迴圈判斷結束條件
final int putIndex = this.putIndex;
//執行迴圈
for (int i = removeIndex;;) {
//獲取要刪除節點索引的下一個索引
int next = i + 1;
//判斷是否已為陣列長度,如果是從陣列頭部(索引為0)開始找
if (next == items.length)
next = 0;
//如果查詢的索引不等於要新增元素的索引,說明元素可以再移動
if (next != putIndex) {
items[i] = items[next];//把後一個元素前移覆蓋要刪除的元
i = next;
} else {
//在removeIndex索引之後的元素都往前移動完畢後清空最後一個元素
items[i] = null;
this.putIndex = i;
break;//結束迴圈
}
}
count--;//佇列元素減1
if (itrs != null)
itrs.removedAt(removeIndex);//更新迭代器資料
}
notFull.signal();//喚醒新增執行緒
}
take方法其實很簡單,有就刪除沒有就阻塞,只不過這個阻塞是可以中斷的,如果佇列沒有資料那麼就加入notEmpty條件佇列等待(有資料就直接取走,方法結束),如果有新的put執行緒新增了資料,那麼put操作將會喚醒take執行緒,執行take操作。
//從佇列頭部刪除,佇列沒有元素就阻塞,可中斷
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//中斷
try {
//如果佇列沒有元素
while (count == 0)
//執行阻塞操作
notEmpty.await();
return dequeue();//如果佇列有元素執行刪除操作
} finally {
lock.unlock();
}
}
peek方法直接返回當前佇列的頭元素但不刪除任何元素。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//直接返回當前佇列的頭元素,但不刪除
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
final E itemAt(int i) {
return (E) items[i];
}
LinkedBlockingQueue
LinkedBlockingQueue是一個基於連結串列的阻塞佇列,其內部維持一個基於連結串列的資料佇列,使用兩把鎖(takeLock,putLock)允許讀寫並行,remove和iterator時需要同時獲取兩把鎖。
LinkedBlockingQueue預設為無界佇列,即大小為Integer.MAX_VALUE,如果消費者速度慢於生產者速度,可能造成記憶體空間不足,建議手動設定佇列大小。
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
/**
* 節點類,用於儲存資料
*/
static class Node<E> {
E item;
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;
/** 獲取並移除元素時使用的鎖,如take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** notEmpty條件物件,當佇列沒有資料時用於掛起執行刪除的執行緒 */
private final Condition notEmpty = takeLock.newCondition();
/** 新增元素時使用的鎖如 put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** notFull條件物件,當佇列資料已滿時用於掛起執行新增的執行緒 */
private final Condition notFull = putLock.newCondition();
}
每次插入操作都將動態構造Linked nodes。
每個新增到LinkedBlockingQueue佇列中的資料都將被封裝成Node節點,新增的連結串列佇列中,其中head和last分別指向佇列的頭結點和尾結點。
與ArrayBlockingQueue不同的是,LinkedBlockingQueue內部分別使用了takeLock 和 putLock 對併發進行控制,也就是說,新增和刪除操作並不是互斥操作,可以同時進行,這樣也就可以大大提高吞吐量。
建構函式
1、LinkedBlockingQueue():初始化容量為Integer.MAX_VALUE的佇列;
2、LinkedBlockingQueue(int capacity):指定佇列容量並初始化頭尾節點
3、LinkedBlockingQueue(Collection c):初始化一個容量為Integer.MAX_VALUE且包含集合c所有元素的佇列,且阻塞佇列的迭代順序同集合c。若集合c元素包含null,將throwNullPointerException;若集合c元素個數達到Integer.MAX_VALUE,將throwIllegalStateException(“Queue full”)。
// 將node連結到佇列尾部
private void enqueue(Node<E> node) { // 入隊
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node; // 等價於last.next = node;last = last.next(即node)
}
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended(競爭), but necessary for visibility(可見性)
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e)); // 執行last = last.next = node;
++n;
}
count.set(n); // 設定佇列元素個數
} finally {
putLock.unlock();
}
}
新增元素阻塞
新增元素主要有這幾個方法add、offer、put
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public boolean offer(E e) {
//新增元素為null直接丟擲異常
if (e == null) throw new NullPointerException();
//獲取佇列的個數
final AtomicInteger count = this.count;
//判斷佇列是否已滿
if (count.get() == capacity)
return false;
int c = -1;
//構建節點
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
//再次判斷佇列是否已滿,考慮併發情況
if (count.get() < capacity) {
enqueue(node);//新增元素
c = count.getAndIncrement();//拿到當前未新增新元素時的佇列長度
//如果容量還沒滿
if (c + 1 < capacity)
notFull.signal();//喚醒下一個新增執行緒,執行新增操作
}
} finally {
putLock.unlock();
}
// 由於存在新增鎖和消費鎖,而消費鎖和新增鎖都會持續喚醒等到執行緒,因此count肯定會變化。
//這裡的if條件表示如果佇列中還有1條資料
if (c == 0)
signalNotEmpty();//如果還存在資料那麼就喚醒消費鎖
return c >= 0; // 新增成功返回true,否則返回false
}
//入隊操作
private void enqueue(Node<E> node) {
//佇列尾節點指向新的node節點
last = last.next = node;
}
//signalNotEmpty方法
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
//喚醒獲取並刪除元素的執行緒
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
這裡的Offer()方法做了兩件事:
- 判斷佇列是否滿,滿了就直接釋放鎖,沒滿就將節點封裝成Node入隊,然後再次判斷佇列新增完成後是否已滿,不滿就繼續喚醒等到在條件物件notFull上的新增執行緒。
- 判斷是否需要喚醒等到在notEmpty條件物件上的消費執行緒。
為什麼新增完成後是繼續喚醒在條件物件notFull上的新增執行緒而不是像ArrayBlockingQueue那樣直接喚醒notEmpty條件物件上的消費執行緒?為什麼要當if (c == 0)時才去喚醒消費執行緒呢?
第一個疑問:在新增新元素完成後,會判斷佇列是否已滿,不滿就繼續喚醒在條件物件notFull上的新增執行緒,這點與前面分析的ArrayBlockingQueue很不相同,在ArrayBlockingQueue內部完成新增操作後,會直接喚醒消費執行緒對元素進行獲取,這是因為ArrayBlockingQueue只用了一個ReenterLock同時對新增執行緒和消費執行緒進行控制,這樣如果在新增完成後再次喚醒新增執行緒的話,消費執行緒可能永遠無法執行。
而對於LinkedBlockingQueue來說就不一樣了,其內部對新增執行緒和消費執行緒分別使用了各自的ReenterLock鎖對併發進行控制,也就是說新增執行緒和消費執行緒是不會互斥的,所以新增鎖只要管好自己的新增執行緒即可,新增執行緒自己直接喚醒自己的其他新增執行緒,如果沒有等待的新增執行緒,直接結束了。
如果有就直到佇列元素已滿才結束掛起,當然offer方法並不會掛起,而是直接結束,只有put方法才會當佇列滿時才執行掛起操作。注意消費執行緒的執行過程也是如此。這也是為什麼LinkedBlockingQueue的吞吐量要相對大些的原因。
第二個疑問:消費執行緒一旦被喚醒是一直在消費的(前提是有資料),所以c值是一直在變化的,c值是新增完元素前佇列的大小,此時c只可能是0或c>0,如果是c=0,那麼說明之前消費執行緒已停止,條件物件上可能存在等待的消費執行緒,新增完資料後應該是c+1,那麼有資料就直接喚醒等待消費執行緒,如果沒有就結束啦,等待下一次的消費操作。如果c>0那麼消費執行緒就不會被喚醒,只能等待下一個消費操作(poll、take、remove)的呼叫,那為什麼不是條件c>0才去喚醒呢?我們要明白的是消費執行緒一旦被喚醒會和新增執行緒一樣,一直不斷喚醒其他消費執行緒,如果新增前c>0,那麼很可能上一次呼叫的消費執行緒後,資料並沒有被消費完,條件佇列上也就不存在等待的消費執行緒了,所以c>0喚醒消費執行緒得意義不是很大,當然如果新增執行緒一直新增元素,那麼一直c>0,消費執行緒執行的換就要等待下一次呼叫消費操作了(poll、take、remove)。
根據時間阻塞
//在指定時間內阻塞新增的方法,超時就結束
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
//將時間轉換成納秒
long nanos = unit.toNanos(timeout);
int c = -1;
//獲取鎖
final ReentrantLock putLock = this.putLock;
//獲取當前佇列大小
final AtomicInteger count = this.count;
//鎖中斷(如果需要)
putLock.lockInterruptibly();
try {
//判斷佇列是否滿
while (count.get() == capacity) {
if (nanos <= 0)
return false;
//如果佇列滿根據阻塞的等待
nanos = notFull.awaitNanos(nanos);
}
//佇列沒滿直接入隊
enqueue(new Node<E>(e));
c = count.getAndIncrement();
//喚醒條件物件上等待的執行緒
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
//喚醒消費執行緒
if (c == 0)
signalNotEmpty();
return true;
}
//CoditionObject(Codition的實現類)中的awaitNanos方法
public final long awaitNanos(long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//這裡是將當前新增執行緒封裝成NODE節點加入Condition的等待佇列中
//注意這裡的NODE是AQS的內部類Node
Node node = addConditionWaiter();
//加入等待,那麼就釋放當前執行緒持有的鎖
int savedState = fullyRelease(node);
//計算過期時間
final long deadline = System.nanoTime() + nanosTimeout;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
if (nanosTimeout <= 0L) {
transferAfterCancelledWait(node);
break;
}
//主要看這裡!!由於是while 迴圈,這裡會不斷判斷等待時間
//nanosTimeout 是否超時
//static final long spinForTimeoutThreshold = 1000L;
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);//掛起執行緒
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
//重新計算剩餘等待時間,while迴圈中繼續判斷下列公式
//nanosTimeout >= spinForTimeoutThreshold
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return deadline - System.nanoTime();
}
據傳遞進來的時間計算超時阻塞nanosTimeout,然後通過while迴圈中判斷nanosTimeout >= spinForTimeoutThreshold 該公式是否成立,當其為true時則說明超時時間nanosTimeout 還未到期,再次計算nanosTimeout = deadline - System.nanoTime();即nanosTimeout ,持續判斷,直到nanosTimeout 小於spinForTimeoutThreshold結束超時阻塞操作,方法也就結束。
這裡的spinForTimeoutThreshold其實更像一個經驗值,因為非常短的超時等待無法做到十分精確,因此採用了spinForTimeoutThreshold這樣一個臨界值。offer(E e, long timeout, TimeUnit unit)方法內部正是利用這樣的Codition的超時等待awaitNanos方法實現新增方法的超時阻塞操作。同樣對於poll(long timeout, TimeUnit unit)方法也是一樣的道理。
移除元素阻塞
移除的方法主要有remove、poll、take
remove方法刪除指定的物件。由於remove方法刪除的資料的位置不確定,為了避免造成並非安全問題,所以需要同時對putLock和takeLock加鎖。
public boolean remove(Object o) {
if (o == null) return false;
fullyLock();//同時對putLock和takeLock加鎖
try {
//迴圈查詢要刪除的元素
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {//找到要刪除的節點
unlink(p, trail);//直接刪除
return true;
}
}
return false;
} finally {
fullyUnlock();//解鎖
}
}
//兩個同時加鎖
void fullyLock() {
putLock.lock();
takeLock.lock();
}
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
poll方法比較簡單,如果佇列沒有資料就返回null,如果佇列有資料,那麼就取出來,如果佇列還有資料那麼喚醒等待在條件物件notEmpty上的消費執行緒。然後判斷if (c == capacity)為true就喚醒新增執行緒,這點與前面分析if(c==0)是一樣的道理。因為只有可能佇列滿了,notFull條件物件上才可能存在等待的新增執行緒。
public E poll() {
//獲取當前佇列的大小
final AtomicInteger count = this.count;
if (count.get() == 0)//如果沒有元素直接返回null
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//判斷佇列是否有資料
if (count.get() > 0) {
//如果有,直接刪除並獲取該元素值
x = dequeue();
//當前佇列大小減一
c = count.getAndDecrement();
//如果佇列未空,繼續喚醒等待在條件物件notEmpty上的消費執行緒
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
//判斷c是否等於capacity,這是因為如果滿說明NotFull條件物件上
//可能存在等待的新增執行緒
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
Node<E> h = head;//獲取頭結點
Node<E> first = h.next; 獲取頭結的下一個節點(要刪除的節點)
h.next = h; // help GC//自己next指向自己,即被刪除
head = first;//更新頭結點
E x = first.item;//獲取刪除節點的值
first.item = null;//清空資料,因為first變成頭結點是不能帶資料的,這樣也就刪除佇列的帶資料的第一個節點
return x;
}
take方法是一個可阻塞可中斷的移除方法主要做了兩件事
如果佇列沒有資料就掛起當前執行緒到 notEmpty條件物件的等待佇列中一直等待,如果有資料就刪除節點並返回資料項,同時喚醒後續消費執行緒,嘗試喚醒條件物件notFull上等待佇列中的新增執行緒。
移除方法中只有take方法具備阻塞功能。remove方法是成功返回true失敗返回false,poll方法成功返回被移除的值,失敗或沒資料返回null。
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();//佇列大小減1
if (c > 1)
notEmpty.signal();//還有資料就喚醒後續的消費執行緒
} finally {
takeLock.unlock();
}
//滿足條件,喚醒條件物件上等待佇列中的新增執行緒
if (c == capacity)
signalNotFull();
return x;
}
lock 與 lockInterruptibly的區別
lock優先考慮獲取鎖,待獲取鎖成功後,才響應中斷。
lockInterruptibly 優先考慮響應中斷,而不是響應鎖的普通獲取或重入獲取。
詳細區別:
ReentrantLock.lockInterruptibly允許在等待時由其它執行緒呼叫等待執行緒的Thread.interrupt方法來中斷等待執行緒的等待而直接返回,這時不用獲取鎖,而會丟擲一個InterruptedException。
ReentrantLock.lock方法不允許Thread.interrupt中斷,即使檢測到Thread.isInterrupted,一樣會繼續嘗試獲取鎖,失敗則繼續休眠。只是在最後獲取鎖成功後再把當前執行緒置為interrupted狀態,然後再中斷執行緒。
執行緒喚醒
對notEmpty和notFull的喚醒操作均使用的是signal()而不是signalAll()。
signalAll() 雖然能喚醒Condition上所有等待的執行緒,但卻並不見得會節省資源,相反,喚醒操作會帶來上下文切換,且會有鎖的競爭。此外,由於此處獲取的鎖均是同一個(putLock或takeLock),同一時刻被鎖的執行緒只有一個,也就無從談起喚醒多個執行緒了。
LinkedBlockingQueue與ArrayBlockingQueue比較
ArrayBlockingQueue底層基於陣列,建立時必須指定佇列大小,“有界”LinkedBlockingQueue“無界”,節點動態建立,節點出隊後可被GC,故伸縮性較好;
ArrayBlockingQueue入隊和出隊使用同一個lock(但資料讀寫操作已非常簡潔),讀取和寫入操作無法並行,LinkedBlockingQueue使用雙鎖可並行讀寫,其吞吐量更高。
ArrayBlockingQueue在插入或刪除元素時直接放入陣列指定位置(putIndex、takeIndex),不會產生或銷燬任何額外的物件例項;而LinkedBlockingQueue則會生成一個額外的Node物件,在高效併發處理大量資料時,對GC的影響存在一定的區別。
相關文章
- 阻塞佇列 BlockingQueue佇列BloC
- 阻塞佇列--LinkedBlockingQueue佇列BloC
- 阻塞佇列BlockingQueue(三)--DelayQueue佇列BloC
- Java併發系列 — 阻塞佇列(BlockingQueue)Java佇列BloC
- 用Java如何設計一個阻塞佇列,然後說說ArrayBlockingQueue和LinkedBlockingQueueJava佇列BloC
- 併發佇列ConcurrentLinkedQueue和阻塞佇列LinkedBlockingQueue用法佇列BloC
- Java執行緒(篇外篇):阻塞佇列BlockingQueueJava執行緒佇列BloC
- 迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析JavaBloC佇列
- Java 阻塞佇列(BlockingQueue)的內部實現原理Java佇列BloC
- Java BlockingQueue 阻塞佇列[用於多執行緒]JavaBloC佇列執行緒
- Java併發指南11:解讀 Java 阻塞佇列 BlockingQueueJava佇列BloC
- 佇列、阻塞佇列佇列
- 阻塞佇列一——java中的阻塞佇列佇列Java
- 阻塞佇列佇列
- BlockingQueue的作用以及實現的幾個常用阻塞佇列原理BloC佇列
- ArrayBlockingQueue 和 LinkedBlockingQueue 效能測試與分析BloC
- Java LinkedBlockingQueue和ArrayBlockingQueue分析JavaBloC
- 死磕阻塞佇列佇列
- java併發程式設計工具類JUC第一篇:BlockingQueue阻塞佇列Java程式設計BloC佇列
- Java併發包原始碼學習系列:阻塞佇列BlockingQueue及實現原理分析Java原始碼佇列BloC
- 解讀 Java 併發佇列 BlockingQueueJava佇列BloC
- Java併發包原始碼學習系列:阻塞佇列實現之ArrayBlockingQueue原始碼解析Java原始碼佇列BloC
- 同步容器、併發容器、阻塞佇列、雙端佇列與工作密取佇列
- Java中的阻塞佇列Java佇列
- 阻塞佇列——四組API佇列API
- 延遲阻塞佇列 DelayQueue佇列
- Java併發包原始碼學習系列:阻塞佇列實現之LinkedBlockingQueue原始碼解析Java原始碼佇列BloC
- 從原始碼解析-Android資料結構之單向阻塞佇列LinkedBlockingQueue的使用原始碼Android資料結構佇列BloC
- 併發佇列ConcurrentLinkedQueue與LinkedBlockingQueue原始碼分析與對比佇列BloC原始碼
- 乾貨|解讀Java併發佇列BlockingQueueJava佇列BloC
- 處理線上RabbitMQ佇列阻塞MQ佇列
- 聊聊併發(四)——阻塞佇列佇列
- 阻塞佇列 SynchronousQueue 原始碼解析佇列原始碼
- 阻塞佇列 DelayQueue 原始碼解析佇列原始碼
- [Java併發程式設計實戰] 阻塞佇列 BlockingQueue(含程式碼,生產者-消費者模型)Java程式設計佇列BloC模型
- Java併發——阻塞佇列集(下)Java佇列
- Java併發——阻塞佇列集(上)Java佇列
- 阻塞佇列 LinkedTransferQueue 原始碼解析佇列原始碼