上一篇我們學習了ArrayBlockingQueue的實現原理,這一篇我們來學習與之對應的LinkedBlockingQueue。很明顯,ArrayBlockingQueue內部是基於數組實現的,而LinkedBlockingQueue是基於鏈表。他們都實現了阻塞隊列的take和put方法,下面我們會結合ArrayBlockingQueue作對比,來分析LinkedBlockingQueue的實現原理。
1 LinkedBlockingQueue簡介
Integer.MAX_VALUE
。除非插入節點會使隊列超出容量,否則每次插入後會動態地創建鏈接節點。
2 LinkedBlockingQueue類圖結構
3 構造方法
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
4 添加元素
offer方法
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity) //隊列已滿,則返回false
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(); //解鎖
}
if (c == 0) //如果寫入成功
signalNotEmpty(); //通知讀取線程
return c >= 0;
}
這裏討論下,爲什麼插入操作後也要通知寫入線程:因爲A、B線程同時寫,A獲得鎖,B被阻塞,因此,A完成後需要立刻通知B線程寫入,而不是等到讀取線程給B通知。
put方法
將指定元素插入到此隊列的尾部,如有必要,則等待空間變得可用。源碼如下:
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); //如果中斷,則拋出異常並退出。避免了取得鎖後調用wait時才發現中斷
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) { //每次喚醒則獲取鎖 檢查容量 容量已滿則繼續阻塞
notFull.await();
}
enqueue(node); //容量未滿 插入操作
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); //未滿 通知插入線程
} finally {
putLock.unlock(); //解鎖
}
if (c == 0)
signalNotEmpty(); //通知讀取線程
}
5 獲取元素
peek方法
獲取但不移除頭元素,若爲空返回null。源碼如下:
public E peek() {
if (count.get() == 0) //元素爲空返回null
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock(); //獲取鎖
try {
Node<E> first = head.next; //頭結點的next即爲第一個結點
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
peek方法從頭節點直接就可以獲取到第一個添加的元素,所以效率是比較高的。如果不存在則返回null。
poll方法
poll方法獲取並移除此隊列的頭,如果此隊列爲空,則返回 null。源碼如下:
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
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(); //更新count
if (c > 1)
notEmpty.signal(); //通知其他讀取線程
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull(); //通知寫線程
return x;
}
take方法
獲取並移除此隊列的頭部,在元素變得可用之前一直等待(如果有必要)。源碼如下:
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();
if (c > 1) //若隊列中還有元素 則喚醒其他讀取線程
notEmpty.signal();
} finally {
takeLock.unlock(); //解鎖
}
if (c == capacity) //若不滿 喚醒插入線程
signalNotFull();
return x;
}
6 總結
ArrayBlockingQueue和LinkedBlockingQueue的不同
1)內部實現不同
ArrayBlockingQueue使用數組,而LinkedBlockingQueue使用鏈表。
2)鎖的實現不同
ArrayBlockingQueue入隊出隊都使用同一把鎖,而LinkedBlockingQueue使用了兩把鎖。因此,ArrayBlockingQueue的入隊出隊操作是同步的,而LinkedBlockingQueue是可以並行的。這裏的根本原因是:插入操作時,LinkedBlockingQueue的head不會影響head節點,而出隊操作也不會影響tail節點。所以可以並行。
3)初始化條件不同
ArrayBlockingQueue需要確定隊列大小而LinkedBlockingQueue不需要,具有默認值Integer.Max_VALUE。