前言
整理了阻塞佇列LinkedBlockingQueue的學習筆記,希望對大家有幫助。有哪裡不正確,歡迎指出,感謝。
LinkedBlockingQueue的概述
LinkedBlockingQueue的繼承體系圖
我們先來看看LinkedBlockingQueue的繼承體系。使用IntelliJ IDEA檢視類的繼承關係圖形
- 藍色實線箭頭是指類繼承關係
- 綠色箭頭實線箭頭是指介面繼承關係
- 綠色虛線箭頭是指介面實現關係。
LinkedBlockingQueue實現了序列化介面 Serializable,因此它有序列化的特性。 LinkedBlockingQueue實現了BlockingQueue介面,BlockingQueue繼承了Queue介面,因此它擁有了佇列Queue相關方法的操作。
LinkedBlockingQueue的類圖
類圖來自Java併發程式設計之美
LinkedBlockingQueue主要特性:
- LinkedBlockingQueue底層資料結構為單向連結串列。
- LinkedBlockingQueue 有兩個Node節點,一個head節點,一個tail節點,只能從head取元素,從tail新增元素。
- LinkedBlockingQueue 容量是一個原子變數count,它的初始值為0。
- LinkedBlockingQueue有兩把ReentrantLock的鎖,一把控制元素入隊,一把控制出隊,保證在併發情況下的執行緒安全。
- LinkedBlockingQueue 有兩個條件變數,notEmpty 和 notFull。它們內部均有一個條件佇列,存放著出入佇列被阻塞的執行緒,這其實是生產者-消費者模型。
LinkedBlockingQueue的重要成員變數
//容量範圍,預設值為 Integer.MAX_VALUE
private final int capacity;
//當前佇列元素個數
private final AtomicInteger count = new AtomicInteger();
//頭結點
transient Node<E> head;
//尾節點
private transient Node<E> last;
//take, poll等方法的可重入鎖
private final ReentrantLock takeLock = new ReentrantLock();
//當佇列為空時,執行出隊操作(比如take )的執行緒會被放入這個條件佇列進行等待
private final Condition notEmpty = takeLock.newCondition();
//put, offer等方法的可重入鎖
private final ReentrantLock putLock = new ReentrantLock();
//當佇列滿時, 執行進隊操作( 比如put)的執行緒會被放入這個條件佇列進行等待
private final Condition notFull = putLock.newCondition();
複製程式碼
LinkedBlockingQueue的建構函式
LinkedBlockingQueue有三個建構函式:
- 無參建構函式,容量為Integer.MAX
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
複製程式碼
- 設定指定容量的構造器
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
//設定佇列大小
this.capacity = capacity;
//new一個null節點,head、tail節點指向該節點
last = head = new Node<E>(null);
}
複製程式碼
- 傳入集合,如果呼叫該構造器,容量預設也是Integer.MAX_VALUE
public LinkedBlockingQueue(Collection<? extends E> c) {
//呼叫指定容量的構造器
this(Integer.MAX_VALUE);
//獲取put, offer的可重入鎖
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
int n = 0;
//迴圈向佇列中新增集合中的元素
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
//將佇列的last節點指向該節點
enqueue(new Node<E>(e));
++n;
}
//更新容量值
count.set(n);
} finally {
//釋放鎖
putLock.unlock();
}
}
複製程式碼
LinkedBlockingQueue底層Node類
Node原始碼
static class Node<E> {
// 當前節點的元素值
E item;
// 下一個節點的索引
Node<E> next;
//節點構造器
Node(E x) {
item = x;
}
}
複製程式碼
LinkedBlockingQueue的節點符合單向連結串列的資料結構要求:
- 一個成員變數為當前節點的元素值
- 一個成員變數是下一節點的索引
- 構造方法的唯一引數節點元素值。
Node節點圖
item表示當前節點的元素值,next表示指向下一節點的指標
LinkedBlockingQueue常用操作
offer操作
入隊方法,其實就是向佇列的尾部插入一個元素。如果元素為空,丟擲空指標異常。如果佇列已滿,則丟棄當前元素,返回false,它是非阻塞的。如果佇列空閒則插入成功返回true。
offer原始碼
offer方法原始碼如下:
public boolean offer(E e) {
//為空直接拋空指標
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
//如果當前佇列滿了的話,直接返回false
if (count.get() == capacity)
return false;
int c = -1;
//構造新節點
Node<E> node = new Node<E>(e);
獲取put獨佔鎖
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
//判斷佇列是否已滿
if (count.get() < capacity) {
//進佇列
enqueue(node);
//遞增元素計數
c = count.getAndIncrement();
//如果元素入隊,還有空閒,則喚醒notFull條件佇列裡被阻塞的執行緒
if (c + 1 < capacity)
notFull.signal();
}
} finally {
//釋放鎖
putLock.unlock();
}
//如果容量為0,則
if (c == 0)
//啟用 notEmpty 的條件佇列,喚醒被阻塞的執行緒
signalNotEmpty();
return c >= 0;
}
複製程式碼
enqueue方法原始碼如下:
private void enqueue(Node<E> node) {
//從尾節點加進去
last = last.next = node;
}
複製程式碼
為了形象生動,我們用一張圖來看看往佇列裡依次放入元素A和元素B。圖片參考來源【細談Java併發】談談LinkedBlockingQueue
signalNotEmpty方法原始碼如下
private void signalNotEmpty() {
//獲取take獨佔鎖
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//喚醒notEmpty條件佇列裡被阻塞的執行緒
notEmpty.signal();
} finally {
//釋放鎖
takeLock.unlock();
}
}
複製程式碼
offer執行流程圖
基本流程:
- 判斷元素是否為空,如果是,就丟擲空指標異常。
- 判讀佇列是否已滿,如果是,新增失敗,返回false。
- 如果佇列沒滿,構造Node節點,上鎖。
- 判斷佇列是否已滿,如果佇列沒滿,Node節點在隊尾加入佇列待。
- 加入佇列後,判斷佇列是否還有空閒,如果是,喚醒notFull的阻塞執行緒。
- 釋放完鎖後,判斷容量是否為空,如果是,喚醒notEmpty的阻塞執行緒。
put操作
put方法也是向佇列尾部插入一個元素。如果元素為null,丟擲空指標異常。如果佇列己滿則阻塞當前執行緒,直到佇列有空閒插入成功為止。如果佇列空閒則插入成功,直接返回。如果在阻塞時被其他執行緒設定了中斷標誌, 則被阻塞執行緒會丟擲 InterruptedException 異常而返回。
put原始碼
public void put(E e) throws InterruptedException {
////為空直接拋空指標異常
if (e == null) throw new NullPointerException();
int c = -1;
// 構造新節點
Node<E> node = new Node<E>(e);
//獲取putLock獨佔鎖
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//獲取獨佔鎖,它跟lock的區別,是可以被中斷
putLock.lockInterruptibly();
try {
//佇列已滿執行緒掛起等待
while (count.get() == capacity) {
notFull.await();
}
//進佇列
enqueue(node);
//遞增元素計數
c = count.getAndIncrement();
//如果元素入隊,還有空閒,則喚醒notFull條件佇列裡被阻塞的執行緒
if (c + 1 < capacity)
notFull.signal();
} finally {
//釋放鎖
putLock.unlock();
}
//如果容量為0,則
if (c == 0)
//啟用 notEmpty 的條件佇列,喚醒被阻塞的執行緒
signalNotEmpty();
}
複製程式碼
put流程圖
基本流程:
- 判斷元素是否為空,如果是就丟擲空指標異常。
- 構造Node節點,上鎖(可中斷鎖)
- 判斷佇列是否已滿,如果是,阻塞當前執行緒,一直等待。
- 如果佇列沒滿,Node節點在隊尾加入佇列。
- 加入佇列後,判斷佇列是否還有空閒,如果是,喚醒notFull的阻塞執行緒。
- 釋放完鎖後,判斷容量是否為空,如果是,喚醒notEmpty的阻塞執行緒。
poll操作
從佇列頭部獲取並移除一個元素, 如果佇列為空則返回 null, 該方法是不阻塞的。
poll原始碼
poll方法原始碼
public E poll() {
final AtomicInteger count = this.count;
//如果佇列為空,返回null
if (count.get() == 0)
return null;
E x = null;
int c = -1;
//獲取takeLock獨佔鎖
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//如果佇列不為空,則出隊,並遞減計數
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
////容量大於1,則啟用 notEmpty 的條件佇列,喚醒被阻塞的執行緒
if (c > 1)
notEmpty.signal();
}
} finally {
//釋放鎖
takeLock.unlock();
}
if (c == capacity)
//喚醒notFull條件佇列裡被阻塞的執行緒
signalNotFull();
return x;
}
複製程式碼
dequeue方法原始碼
//出佇列
private E dequeue() {
//獲取head節點
Node<E> h = head;
//獲取到head節點指向的下一個節點
Node<E> first = h.next;
//head節點原來指向的節點的next指向自己,等待下次gc回收
h.next = h; // help GC
// head節點指向新的節點
head = first;
// 獲取到新的head節點的item值
E x = first.item;
// 新head節點的item值設定為null
first.item = null;
return x;
}
複製程式碼
為了形象生動,我們用一張圖來描述出隊過程。圖片參考來源【細談Java併發】談談LinkedBlockingQueue
signalNotFull方法原始碼
private void signalNotFull() {
//獲取put獨佔鎖
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
////喚醒notFull條件佇列裡被阻塞的執行緒
notFull.signal();
} finally {
//釋放鎖
putLock.unlock();
}
}
複製程式碼
poll流程圖
基本流程:
- 判斷元素是否為空,如果是,就返回null。
- 加鎖
- 判斷佇列是否有元素,如果沒有,釋放鎖
- 如果佇列有元素,則出佇列,獲取資料,容量計數器減一。
- 判斷此時容量是否大於1,如果是,喚醒notEmpty的阻塞執行緒。
- 釋放完鎖後,判斷容量是否滿,如果是,喚醒notFull的阻塞執行緒。
peek操作
獲取佇列頭部元素但是不從佇列裡面移除它,如果佇列為空則返回 null。 該方法是不 阻塞的。
peek原始碼
public E peek() {
//佇列容量為0,返回null
if (count.get() == 0)
return null;
//獲取takeLock獨佔鎖
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
//判斷first是否為null,如果是直接返回
if (first == null)
return null;
else
return first.item;
} finally {
//釋放鎖
takeLock.unlock();
}
}
複製程式碼
peek流程圖
基本流程:
- 判斷佇列容量大小是否為0,如果是,就返回null。
- 加鎖
- 獲取佇列頭部節點first
- 判斷節點first是否為null,是的話,返回null。
- 如果fist不為null,返回節點first的元素。
- 釋放鎖。
take操作
獲取當前佇列頭部元素並從佇列裡面移除它。 如果佇列為空則阻塞當前執行緒直到佇列 不為空然後返回元素,如果在阻塞時被其他執行緒設定了中斷標誌, 則被阻塞執行緒會丟擲 InterruptedException 異常而返回。
take原始碼
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
//獲取takeLock獨佔鎖
final ReentrantLock takeLock = this.takeLock;
//獲取獨佔鎖,它跟lock的區別,是可以被中斷
takeLock.lockInterruptibly();
try {
//當前佇列為空,則阻塞掛起
while (count.get() == 0) {
notEmpty.await();
}
//)出隊並遞減計數
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
//啟用 notEmpty 的條件佇列,喚醒被阻塞的執行緒
notEmpty.signal();
} finally {
//釋放鎖
takeLock.unlock();
}
if (c == capacity)
//啟用 notFull 的條件佇列,喚醒被阻塞的執行緒
signalNotFull();
return x;
}
複製程式碼
take流程圖
基本流程:
- 加鎖
- 判斷佇列容量大小是否為0,如果是,阻塞當前執行緒,直到佇列不為空。
- 如果佇列容量大小大於0,節點出佇列,獲取元素x,計數器減一。
- 判斷佇列容量大小是否大於1,如果是,喚醒notEmpty的阻塞執行緒。
- 釋放鎖。
- 判斷佇列容量是否已滿,如果是,喚醒notFull的阻塞執行緒。
- 返回出隊元素x
remove操作
刪除佇列裡面指定的元素,有則刪除並返回 true,沒有則返回 false。
remove方法原始碼
public boolean remove(Object o) {
//為空直接返回false
if (o == null) return false;
//雙重加鎖
fullyLock();
try {
//邊歷佇列,找到元素則刪除並返回true
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
//執行unlink操作
unlink(p, trail);
return true;
}
}
return false;
} finally {
//解鎖
fullyUnlock();
}
}
複製程式碼
雙重加鎖,fullyLock方法原始碼
void fullyLock() {
//putLock獨佔鎖加鎖
putLock.lock();
//takeLock獨佔鎖加鎖
takeLock.lock();
}
複製程式碼
unlink方法原始碼
void unlink(Node<E> p, Node<E> trail) {
p.item = null;
trail.next = p.next;
if (last == p)
last = trail;
//如果當前佇列滿 ,則刪除後,也不忘記喚醒等待的執行緒
if (count.getAndDecrement() == capacity)
notFull.signal();
}
複製程式碼
fullyUnlock方法原始碼
void fullyUnlock() {
//與雙重加鎖順序相反,先解takeLock獨佔鎖
takeLock.unlock();
putLock.unlock();
}
複製程式碼
remove流程圖
基本流程
- 判斷要刪除的元素是否為空,是就返回false。
- 如果要刪除的元素不為空,加雙重鎖
- 遍歷佇列,找到要刪除的元素,如果找不到,返回false。
- 如果找到,刪除該節點,返回true。
- 釋放鎖
size操作
獲取當前佇列元素個數。
public int size() {
return count.get();
}
複製程式碼
由於進行出隊、入隊操作時的 count是加了鎖的,所以結果相比ConcurrentLinkedQueue 的 size 方法比較準確。
總結
- LinkedBlockingQueue底層通過單向連結串列實現。
- 它有頭尾兩個節點,入隊操作是從尾節點新增元素,出隊操作是對頭節點進行操作。
- 它的容量是原子變數count,保證szie獲取的準確性。
- 它有兩把獨佔鎖,保證了佇列操作原子性。
- 它的兩把鎖都配備了一個條件佇列,用來存放阻塞執行緒,結合入隊、出隊操作實現了一個生產消費模型。
Java併發程式設計之美中,有一張圖惟妙惟肖描述了它,如下圖:
參看與感謝
個人公眾號
- 如果你是個愛學習的好孩子,可以關注我公眾號,一起學習討論。
- 如果你覺得本文有哪些不正確的地方,可以評論,也可以關注我公眾號,私聊我,大家一起學習進步哈。