原文鏈接:深入剖析java并發之阻塞隊列LinkedBlockingQueue與ArrayBlockingQueue - CSDN博客
阻塞隊列概要
阻塞隊列與我們平常接觸的普通隊列(LinkedList或ArrayList等)的最大不同點,在于阻塞隊列支出阻塞添加和阻塞刪除方法。
阻塞添加?
所謂的阻塞添加是指當阻塞隊列元素已滿時,隊列會阻塞加入元素的線程,直隊列元素不滿時才重新喚醒線程執行元素加入操作。
阻塞刪除?
阻塞刪除是指在隊列元素為空時,刪除隊列元素的線程將被阻塞,直到隊列不為空再執行刪除操作(一般都會返回被刪除的元素)
由于Java中的阻塞隊列接口BlockingQueue繼承自Queue接口,因此先來看看阻塞隊列接口為我們提供的主要方法,
public interfaceBlockingQueueextendsQueue {
?????//將指定的元素插入到此隊列的尾部(如果立即可行且不會超過該隊列的容量)
?????//在成功時返回 true,如果此隊列已滿,則拋IllegalStateException。
????boolean add(E e);
????//將指定的元素插入到此隊列的尾部(如果立即可行且不會超過該隊列的容量)
? ? // 將指定的元素插入此隊列的尾部,如果該隊列已滿,
? ? // 則在到達指定的等待時間之前等待可用的空間,該方法可中斷
? ? boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
? ? //將指定的元素插入此隊列的尾部,如果該隊列已滿,則一直等到(阻塞)。
? ? void put(E e) throws InterruptedException;
? ? //獲取并移除此隊列的頭部,如果沒有元素則等待(阻塞),
? ? //直到有元素將喚醒等待線程執行該操作
? ? E take() throws InterruptedException;
? ? //獲取并移除此隊列的頭部,在指定的等待時間前一直等到獲取元素,
? ? //超過時間方法將結束
? ? E poll(long timeout, TimeUnit unit) throws InterruptedException;
? ? //從此隊列中移除指定元素的單個實例(如果存在)。
? ? boolean remove(Object o);
}
? ? //除了上述方法還有繼承自Queue接口的方法
? ? //獲取但不移除此隊列的頭元素,沒有則跑異常NoSuchElementException
? ? E element();
? ? //獲取但不移除此隊列的頭;如果此隊列為空,則返回 null。
? ? E peek();
? ? //獲取并移除此隊列的頭,如果此隊列為空,則返回 null。
? ? E poll();
這里我們把上述操作進行分類
插入方法
add(E e) : 添加成功返回true,失敗拋IllegalStateException異常
offer(E e) : 成功返回 true,如果此隊列已滿,則返回 false。
put(E e) :將元素插入此隊列的尾部,如果該隊列已滿,則一直阻塞
刪除方法
remove(Object o) :移除指定元素,成功返回true,失敗返回false
poll() : 獲取并移除此隊列的頭元素,若隊列為空,則返回 null
take():獲取并移除此隊列頭元素,若沒有元素則一直阻塞。
檢查方法
element() :獲取但不移除此隊列的頭元素,沒有元素則拋異常
peek() :獲取但不移除此隊列的頭;若隊列為空,則返回 null。
阻塞隊列的對元素的增刪查操作主要就是上述的三類方法,通常情況下我們都是通過這3類方法操作阻塞隊列,了解完阻塞隊列的基本方法后,下面我們將分析阻塞隊列中的兩個實現類ArrayBlockingQueue和LinkedBlockingQueue的簡單使用和實現原理,其中實現原理是這篇文章重點分析的內容。
ArrayBlockingQueue的基本使用
ArrayBlockingQueue 是一個用數組實現的有界阻塞隊列,其內部按先進先出的原則對元素進行排序,其中put方法和take方法為添加和刪除的阻塞方法,下面我們通過ArrayBlockingQueue隊列實現一個生產者消費者的案例,通過該案例簡單了解其使用方式。
package com.zejian.concurrencys.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Created by wuzejian on 2017/8/13
*/public classArrayBlockingQueueDemo{
? ? private final static ArrayBlockingQueue queue= new ArrayBlockingQueue<>(1);
? ? public static void main(String[] args){
? ? ? ? new Thread(new Producer(queue)).start();
? ? ? ? new Thread(new Producer(queue)).start();
? ? ? ? new Thread(new Consumer(queue)).start();
? ? ? ? new Thread(new Consumer(queue)).start();
? ? }
}
class Apple {
? ? public Apple(){
? ? }
}
/**
* 生產者線程
*/
class Producer implements Runnable{
? ? private final ArrayBlockingQueue mAbq;
? ? Producer(ArrayBlockingQueue arrayBlockingQueue){
? ? ? ? this.mAbq = arrayBlockingQueue;
? ? }
? ? @Override? ? public void run() {
? ? ? ? while (true) {
? ? ? ? ? ? Produce();
? ? ? ? }
? ? }
? ? private void Produce(){
? ? ? ? try {
? ? ? ? ? ? Apple apple = new Apple();
? ? ? ? ? ? mAbq.put(apple);
? ? ? ? ? ? System.out.println("生產:"+apple);
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
}
/**
* 消費者線程
*/
class Consumer implements Runnable{
? ? private ArrayBlockingQueue mAbq;
? ? Consumer(ArrayBlockingQueue arrayBlockingQueue){
? ? ? ? this.mAbq = arrayBlockingQueue;
? ? }
? ? @Override? ? public void run() {
? ? ? ? while (true){
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? TimeUnit.MILLISECONDS.sleep(1000);
? ? ? ? ? ? ? ? comsume();
? ? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? private void comsume() throws InterruptedException {
? ? ? ? Apple apple = mAbq.take();
? ? ? ? System.out.println("消費Apple="+apple);
? ? }
}
代碼比較簡單, Consumer 消費者和 Producer 生產者,通過ArrayBlockingQueue 隊列獲取和添加元素,其中消費者調用了take()方法獲取元素當隊列沒有元素就阻塞,生產者調用put()方法添加元素,當隊列滿時就阻塞,通過這種方式便實現生產者消費者模式。比直接使用等待喚醒機制或者Condition條件隊列來得更加簡單。執行代碼,打印部分Log如下,
生產:com.zejian.concurrencys.Queue.Apple@109967f
消費Apple=com.zejian.concurrencys.Queue.Apple@109967f
生產:com.zejian.concurrencys.Queue.Apple@269a77
生產:com.zejian.concurrencys.Queue.Apple@1ce746e
消費Apple=com.zejian.concurrencys.Queue.Apple@269a77
消費Apple=com.zejian.concurrencys.Queue.Apple@1ce746e
........
另外一種寫法:
package test;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* @Class: TestOfArrayBlockingQueue
* @Description: java類作用描述
* @Author: hubohua
* @CreateDate: 2018/8/27
*/
public class TestOfArrayBlockingQueue {
static BlockingQueuequeue =new ArrayBlockingQueue(5);
public static void main(String[] args) {
final Product pro =new Product();
Thread producer =new Thread(new Runnable() {
@Override
? ? ? ? ? ? public void run() {
pro.product(queue);
}
});
Thread consumer =new Thread(new Runnable() {
@Override
? ? ? ? ? ? public void run() {
pro.consume(queue);
}
});
producer.start();
consumer.start();
}
}
class Product {
public void consume(BlockingQueue queue) {
if (queue !=null) {
Product product =null;
try {
while ((product = queue.take()) !=null) {// 阻塞如果空了
? ? ? ? ? ? ? ? ? ? Thread.sleep(2000l);
System.out.println("顧客 " + Thread.currentThread().getName() +
" 消耗了一個product: " + product +", 剩余: " + queue.size());
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void product(BlockingQueue queue) {
if (queue !=null) {
Product product =new Product();
while (queue.offer(product)) {// 阻塞如果滿了
? ? ? ? ? ? ? ? try {
Thread.sleep(2000l);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("工廠 " + Thread.currentThread().getName() +" 生產了一個product: " + product +", 剩余: " + queue.size());
}
}
}
}
結果如下:
工廠 Thread-0 生產了一個product: test.Product@a1a1c96, 剩余: 0
顧客 Thread-1 消耗了一個product: test.Product@a1a1c96, 剩余: 0
工廠 Thread-0 生產了一個product: test.Product@a1a1c96, 剩余: 0
顧客 Thread-1 消耗了一個product: test.Product@a1a1c96, 剩余: 1
顧客 Thread-1 消耗了一個product: test.Product@a1a1c96, 剩余: 0
工廠 Thread-0 生產了一個product: test.Product@a1a1c96, 剩余: 0
工廠 Thread-0 生產了一個product: test.Product@a1a1c96, 剩余: 0
顧客 Thread-1 消耗了一個product: test.Product@a1a1c96, 剩余: 1
工廠 Thread-0 生產了一個product: test.Product@a1a1c96, 剩余: 0
顧客 Thread-1 消耗了一個product: test.Product@a1a1c96, 剩余: 0
工廠 Thread-0 生產了一個product: test.Product@a1a1c96, 剩余: 0
顧客 Thread-1 消耗了一個product: test.Product@a1a1c96, 剩余: 1
工廠 Thread-0 生產了一個product: test.Product@a1a1c96, 剩余: 0
顧客 Thread-1 消耗了一個product: test.Product@a1a1c96, 剩余: 1
疑問:為何在顧客消耗了一次product的時候還會導致queue查詢的size為1?
有點需要注意的是ArrayBlockingQueue內部的阻塞隊列是通過重入鎖ReenterLock和Condition條件隊列實現的,所以ArrayBlockingQueue中的元素存在公平訪問與非公平訪問的區別,對于公平訪問隊列,被阻塞的線程可以按照阻塞的先后順序訪問隊列,即先阻塞的線程先訪問隊列。而非公平隊列,當隊列可用時,阻塞的線程將進入爭奪訪問資源的競爭中,也就是說誰先搶到誰就執行,沒有固定的先后順序。創建公平與非公平阻塞隊列代碼如下:
//默認非公平阻塞隊列
ArrayBlockingQueue queue = new ArrayBlockingQueue(2);
//公平阻塞隊列
ArrayBlockingQueue queue1 = new ArrayBlockingQueue(2,true);
//構造方法源碼
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();
}
其他方法如下:
//自動移除此隊列中的所有元素。
void clear()?
//如果此隊列包含指定的元素,則返回 true。
boolean contains(Object o)?
//移除此隊列中所有可用的元素,并將它們添加到給定collection中。
int drainTo(Collection c)
//最多從此隊列中移除給定數量的可用元素,并將這些元素添加到給定collection 中。
int drainTo(Collection c, int maxElements)
//返回在此隊列中的元素上按適當順序進行迭代的迭代器。
Iterator iterator()
//返回隊列還能添加元素的數量
int remainingCapacity()
//返回此隊列中元素的數量。
int size()
//返回一個按適當順序包含此隊列中所有元素的數組。
Object[] toArray()
//返回一個按適當順序包含此隊列中所有元素的數組;返回數組的運行時類型是指定數組的運行時類型。
T[] toArray(T[] a)
ArrayBlockingQueue的實現原理剖析
ArrayBlockingQueue原理概要
ArrayBlockingQueue的內部是通過一個可重入鎖ReentrantLock和兩個Condition條件對象來實現阻塞,這里先看看其內部成員變量,
public class ArrayBlockingQueue?extends AbstractQueue?implements BlockingQueue,java.io.Serializable {
?/** 存儲數據的數組 */
?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;
}
從成員變量可看出,ArrayBlockingQueue內部確實是通過數組對象items來存儲所有的數據,值得注意的是ArrayBlockingQueue通過一個ReentrantLock來同時控制添加線程與移除線程的并發訪問,這點與LinkedBlockingQueue區別很大(稍后會分析)。而對于notEmpty條件對象則是用于存放等待或喚醒調用take方法的線程,告訴他們隊列已有元素,可以執行獲取操作。同理notFull條件對象是用于等待或喚醒調用put方法的線程,告訴它們,隊列未滿,可以執行添加元素的操作。takeIndex代表的是下一個方法(take,poll,peek,remove)被調用時獲取數組元素的索引,putIndex則代表下一個方法(put, offer, or add)被調用時元素添加到數組中的索引。圖示如下,
ArrayBlockingQueue的(阻塞)添加的實現原理
//add方法實現,間接調用了offer(e)
public boolean add(E e) {
? ? ? ? if (offer(e))
? ? ? ? ? ? return true;
? ? ? ? else
? ? ? ? ? ? throw new IllegalStateException("Queue full");
? ? }
//offer方法
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();
}
這里的add方法和offer方法實現比較簡單,其中需要注意的是enqueue(E x)方法,其方法內部通過putIndex索引直接將元素添加到數組items中,這里可能會疑惑的是當putIndex索引大小等于數組長度時,需要將putIndex重新設置為0,這是因為當前隊列執行元素獲取時總是從隊列頭部獲取,而添加元素從中從隊列尾部獲取所以當隊列索引(從0開始)與數組長度相等時,下次我們就需要從數組頭部開始添加了,如下圖演示,
ok~,接著看put方法,它是一個阻塞添加的方法,
//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)方法將元素加入到數組隊列中。到此我們對三個添加方法即put,offer,add都分析完畢,其中offer,add在正常情況下都是無阻塞的添加,而put方法是阻塞添加。這就是阻塞隊列的添加過程。說白了就是當隊列滿時通過條件對象Condtion來阻塞當前調用put方法的線程,直到線程又再次被喚醒執行。總得來說添加線程的執行存在以下兩種情況,一是,隊列已滿,那么新到來的put線程將添加到notFull的條件隊列中等待,二是,有移除線程執行移除操作,移除成功同時喚醒put線程,如下圖所示,
ArrayBlockingQueue的(阻塞)移除實現原理
關于刪除先看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;
? ? }
poll(),獲取并刪除隊列頭元素,隊列沒有數據就返回null,內部通過dequeue()方法刪除頭元素,注釋很清晰,這里不重復了。接著看remove(Object o)方法,
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();//喚醒添加線程
? ? }
remove(Object o)方法的刪除過程相對復雜些,因為該方法并不是直接從隊列頭部刪除元素。首先線程先獲取鎖,再一步判斷隊列count>0,這點是保證并發情況下刪除操作安全執行。接著獲取下一個要添加源的索引putIndex以及takeIndex索引 ,作為后續循環的結束判斷,因為只要putIndex與takeIndex不相等就說明隊列沒有結束。然后通過while循環找到要刪除的元素索引,執行removeAt(i)方法刪除,在removeAt(i)方法中實際上做了兩件事,一是首先判斷隊列頭部元素是否為刪除元素,如果是直接刪除,并喚醒添加線程,二是如果要刪除的元素并不是隊列頭元素,那么執行循環操作,從要刪除元素的索引removeIndex之后的元素都往前移動一個位置,那么要刪除的元素就被removeIndex之后的元素替換,從而也就完成了刪除操作。接著看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方法其實很簡單,有就刪除沒有就阻塞,注意這個阻塞是可以中斷的,如果隊列沒有數據那么就加入notEmpty條件隊列等待(有數據就直接取走,方法結束),如果有新的put線程添加了數據,那么put操作將會喚醒take線程,執行take操作。圖示如下,
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];
}
peek方法非常簡單,直接返回當前隊列的頭元素但不刪除任何元素。ok~,到此對于ArrayBlockingQueue的主要方法就分析完了。
LinkedBlockingQueue的基本概要
LinkedBlockingQueue是一個由鏈表實現的有界隊列阻塞隊列,但大小默認值為Integer.MAX_VALUE,所以我們在使用LinkedBlockingQueue時建議手動傳值,為其提供我們所需的大小,避免隊列過大造成機器負載或者內存爆滿等情況。其構造函數如下,
//默認大小為Integer.MAX_VALUE
public LinkedBlockingQueue() {
? ? ? this(Integer.MAX_VALUE);
}
//創建指定大小為capacity的阻塞隊列
public LinkedBlockingQueue(int capacity) {
? ? if (capacity <= 0) throw new IllegalArgumentException();
? ? this.capacity = capacity;
? ? last = head = new Node(null);
}
//創建大小默認值為Integer.MAX_VALUE的阻塞隊列并添加c中的元素到阻塞隊列
public LinkedBlockingQueue(Collection 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));
? ? ? ? ? ? ++n;
? ? ? ? }
? ? ? ? count.set(n);
? ? } finally {
? ? ? ? putLock.unlock();
? ? }
}
從源碼看,有三種方式可以構造LinkedBlockingQueue,通常情況下,我們建議創建指定大小的LinkedBlockingQueue阻塞隊列。LinkedBlockingQueue隊列也是按 FIFO(先進先出)排序元素。隊列的頭部是在隊列中時間最長的元素,隊列的尾部 是在隊列中時間最短的元素,新元素插入到隊列的尾部,而隊列執行獲取操作會獲得位于隊列頭部的元素。在正常情況下,鏈接隊列的吞吐量要高于基于數組的隊列(ArrayBlockingQueue),因為其內部實現添加和刪除操作使用的兩個ReenterLock來控制并發執行,而ArrayBlockingQueue內部只是使用一個ReenterLock控制并發,因此LinkedBlockingQueue的吞吐量要高于ArrayBlockingQueue。注意LinkedBlockingQueue和ArrayBlockingQueue的API幾乎是一樣的,但它們的內部實現原理不太相同,這點稍后會分析。使用LinkedBlockingQueue,我們同樣也能實現生產者消費者模式。只需把前面ArrayBlockingQueue案例中的阻塞隊列對象換成LinkedBlockingQueue即可。這里限于篇幅就不貼重復代碼了。接下來我們重點分析LinkedBlockingQueue的內部實現原理,最后我們將對ArrayBlockingQueue和LinkedBlockingQueue 做總結,闡明它們間的不同之處。
LinkedBlockingQueue的實現原理剖析
原理概論
LinkedBlockingQueue是一個基于鏈表的阻塞隊列,其內部維持一個基于鏈表的數據隊列,實際上我們對LinkedBlockingQueue的API操作都是間接操作該數據隊列,這里我們先看看LinkedBlockingQueue的內部成員變量,
public class LinkedBlockingQueue?extends AbstractQueue?implements BlockingQueue,java.io.Serializable{
?/**
? ? * 節點類,用于存儲數據
? ? */
? ? static class Node {
? ? ? ? E item;
? ? ? ? /**
? ? ? ? * One of:
? ? ? ? * - the real successor Node
? ? ? ? * - this Node, meaning the successor is head.next
? ? ? ? * - null, meaning there is no successor (this is the last node)
? ? ? ? */
? ? ? ? Node next;
? ? ? ? Node(E x) { item = x; }
? ? }
? ? /** 阻塞隊列的大小,默認為Integer.MAX_VALUE */
? ? private final int capacity;
? ? /** 當前阻塞隊列中的元素個數 */
? ? private final AtomicInteger count = new AtomicInteger();
? ? /**
? ? * 阻塞隊列的頭結點
? ? */
? ? transient Node head;
? ? /**
? ? * 阻塞隊列的尾節點
? ? */
? ? private transient Node 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();
}
從上述可看成,每個添加到LinkedBlockingQueue隊列中的數據都將被封裝成Node節點,添加的鏈表隊列中,其中head和last分別指向隊列的頭結點和尾結點。與ArrayBlockingQueue不同的是,LinkedBlockingQueue內部分別使用了takeLock 和 putLock 對并發進行控制,也就是說,添加和刪除操作并不是互斥操作,可以同時進行,這樣也就可以大大提高吞吐量。這里再次強調如果沒有給LinkedBlockingQueue指定容量大小,其默認值將是Integer.MAX_VALUE,如果存在添加速度大于刪除速度時候,有可能會內存溢出,這點在使用前希望慎重考慮。至于LinkedBlockingQueue的實現原理圖與ArrayBlockingQueue是類似的,除了對添加和移除方法使用單獨的鎖控制外,兩者都使用了不同的Condition條件對象作為等待隊列,用于掛起take線程和put線程。?
ok~,下面我們看看其其內部添加過程和刪除過程是如何實現的。
添加方法的實現原理
對于添加方法,主要指的是add,offer以及put,這里先看看add方法和offer方法的實現,
public boolean add(E e) {
? ? if (offer(e))
? ? ? ? return true;
? ? else
? ? ? ? throw new IllegalStateException("Queue full");
}
從源碼可以看出,add方法間接調用的是offer方法,如果add方法添加失敗將拋出IllegalStateException異常,添加成功則返回true,那么下面我們直接看看offer的相關方法實現,
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 node = new Node(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 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)時才去喚醒消費線程呢?
1. 喚醒添加線程的原因,在添加新元素完成后,會判斷隊列是否已滿,不滿就繼續喚醒在條件對象notFull上的添加線程,這點與前面分析的ArrayBlockingQueue很不相同,在ArrayBlockingQueue內部完成添加操作后,會直接喚醒消費線程對元素進行獲取,這是因為ArrayBlockingQueue只用了一個ReenterLock同時對添加線程和消費線程進行控制,這樣如果在添加完成后再次喚醒添加線程的話,消費線程可能永遠無法執行,而對于LinkedBlockingQueue來說就不一樣了,其內部對添加線程和消費線程分別使用了各自的ReenterLock鎖對并發進行控制,也就是說添加線程和消費線程是不會互斥的,所以添加鎖只要管好自己的添加線程即可,添加線程自己直接喚醒自己的其他添加線程,如果沒有等待的添加線程,直接結束了。如果有就直到隊列元素已滿才結束掛起,當然offer方法并不會掛起,而是直接結束,只有put方法才會當隊列滿時才執行掛起操作。注意消費線程的執行過程也是如此。這也是為什么LinkedBlockingQueue的吞吐量要相對大些的原因。
2. 為什么要判斷if (c == 0)時才去喚醒消費線程呢,這是因為消費線程一旦被喚醒是一直在消費的(前提是有數據),所以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)。
移除方法的實現原理
關于移除的方法主要是指remove和poll以及take方法,下面一一分析。
public boolean remove(Object o) {
? if (o == null) return false;
? ? fullyLock();//同時對putLock和takeLock加鎖
? ? try {
? ? ? ? //循環查找要刪除的元素
? ? ? ? for (Node 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();
? }
remove方法刪除指定的對象,這里我們可能會詫異,為什么同時對putLock和takeLock加鎖?這是因為remove方法刪除的數據的位置不確定,為了避免造成并非安全問題,所以需要對2個鎖同時加鎖。
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 h = head;//獲取頭結點
? ? ? ? Node first = h.next; //獲取頭結的下一個節點(要刪除的節點)
? ? ? ? h.next = h; // help GC//自己next指向自己,即被刪除
? ? ? ? head = first;//更新頭結點
? ? ? ? E x = first.item;//獲取刪除節點的值
? ? ? ? first.item = null;//清空數據,因為first變成頭結點是不能帶數據的,這樣也就刪除隊列的帶數據的第一個節點
? ? ? ? return x;
? ? }
poll方法也比較簡單,如果隊列沒有數據就返回null,如果隊列有數據,那么就取出來,如果隊列還有數據那么喚醒等待在條件對象notEmpty上的消費線程。然后判斷if (c == capacity)為true就喚醒添加線程,這點與前面分析if(c==0)是一樣的道理。因為只有可能隊列滿了,notFull條件對象上才可能存在等待的添加線程。
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;
? ? }
take方法是一個可阻塞可中斷的移除方法,主要做了兩件事,一是,如果隊列沒有數據就掛起當前線程到 notEmpty條件對象的等待隊列中一直等待,如果有數據就刪除節點并返回數據項,同時喚醒后續消費線程,二是嘗試喚醒條件對象notFull上等待隊列中的添加線程。 到此關于remove、poll、take的實現也分析完了,其中只有take方法具備阻塞功能。remove方法則是成功返回true失敗返回false,poll方法成功返回被移除的值,失敗或沒數據返回null。下面再看看兩個檢查方法,即peek和element,
//構造方法,head 節點不存放數據
?public LinkedBlockingQueue(int capacity) {
? ? ? if (capacity <= 0) throw new IllegalArgumentException();
? ? ? this.capacity = capacity;
? ? ? last = head = new Node(null);
? }
public E element() {
? ? ? ? E x = peek();//直接調用peek
? ? ? ? if (x != null)
? ? ? ? ? ? return x;
? ? ? ? else
? ? ? ? ? ? throw new NoSuchElementException();//沒數據拋異常
? ? }
public E peek() {
? ? ? ? if (count.get() == 0)
? ? ? ? ? ? return null;
? ? ? ? final ReentrantLock takeLock = this.takeLock;
? ? ? ? takeLock.lock();
? ? ? ? try {
? ? ? ? ? ? //獲取頭結節點的下一個節點
? ? ? ? ? ? Node first = head.next;
? ? ? ? ? ? if (first == null)
? ? ? ? ? ? ? ? return null;//為null就返回null
? ? ? ? ? ? else
? ? ? ? ? ? ? ? return first.item;//返回值
? ? ? ? } finally {
? ? ? ? ? ? takeLock.unlock();
? ? ? ? }
? ? }
從代碼來看,head頭結節點在初始化時是本身不帶數據的,僅僅作為頭部head方便我們執行鏈表的相關操作。peek返回直接獲取頭結點的下一個節點返回其值,如果沒有值就返回null,有值就返回節點對應的值。element方法內部調用的是peek,有數據就返回,沒數據就拋異常。下面我們最后來看兩個根據時間阻塞的方法,比較有意思,利用的Condition來實現的。
//在指定時間內阻塞添加的方法,超時就結束
?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));
? ? ? ? ? ? c = count.getAndIncrement();
? ? ? ? ? ? //喚醒條件對象上等待的線程
? ? ? ? ? ? if (c + 1 < capacity)
? ? ? ? ? ? ? ? notFull.signal();
? ? ? ? } finally {
? ? ? ? ? ? putLock.unlock();
? ? ? ? }
? ? ? ? //喚醒消費線程
? ? ? ? if (c == 0)
? ? ? ? ? ? signalNotEmpty();
? ? ? ? return true;
? ? }
對于這個offer方法,我們重點來看看阻塞的這段代碼,
//判斷隊列是否滿
?while (count.get() == capacity) {
? ? ? ? if (nanos <= 0)
? ? ? ? ? ? return false;
? ? ? ? //如果隊列滿根據阻塞的等待
? ? ? ? nanos = notFull.awaitNanos(nanos);
? ? }
//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();
? ? ? ? }
awaitNanos方法中,根據傳遞進來的時間計算超時阻塞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)方法也是一樣的道理。
LinkedBlockingQueue和ArrayBlockingQueue迥異
通過上述的分析,對于LinkedBlockingQueue和ArrayBlockingQueue的基本使用以及內部實現原理我們已較為熟悉了,這里我們就對它們兩間的區別來個小結:
1.隊列大小有所不同,ArrayBlockingQueue是有界的初始化必須指定大小,而LinkedBlockingQueue可以是有界的也可以是無界的(Integer.MAX_VALUE),對于后者而言,當添加速度大于移除速度時,在無界的情況下,可能會造成內存溢出等問題。
2.數據存儲容器不同,ArrayBlockingQueue采用的是數組作為數據存儲容器,而LinkedBlockingQueue采用的則是以Node節點作為連接對象的鏈表。
3.由于ArrayBlockingQueue采用的是數組的存儲容器,因此在插入或刪除元素時不會產生或銷毀任何額外的對象實例,而LinkedBlockingQueue則會生成一個額外的Node對象。這可能在長時間內需要高效并發地處理大批量數據的時,對于GC可能存在較大影響。
4.兩者的實現隊列添加或移除的鎖不一樣,ArrayBlockingQueue實現的隊列中的鎖是沒有分離的,即添加操作和移除操作采用的同一個ReenterLock鎖,而LinkedBlockingQueue實現的隊列中的鎖是分離的,其添加采用的是putLock,移除采用的則是takeLock,這樣能大大提高隊列的吞吐量,也意味著在高并發的情況下生產者和消費者可以并行地操作隊列中的數據,以此來提高整個隊列的并發性能。