上一篇博客,我們介紹了ArrayBlockQueue,知道了它是基於數組實現的有界阻塞隊列,既然有基於數組實現的,那麼一定有基於鏈表實現的隊列了,沒錯,當然有,這就是我們今天的主角:LinkedBlockingQueue。ArrayBlockQueue是有界的,那麼LinkedBlockingQueue是有界還是無界的呢?我覺得可以說是有界的,也可以說是無界的,爲什麼這麼說呢?看下去你就知道了。
和上篇博客一樣,我們還是先看下LinkedBlockingQueue的基本應用,然後解析LinkedBlockingQueue的核心代碼。
LinkedBlockingQueue基本應用
public static void main(String[] args) throws InterruptedException {
LinkedBlockingQueue<Integer> linkedBlockingQueue = new LinkedBlockingQueue();
linkedBlockingQueue.add(15);
linkedBlockingQueue.add(60);
linkedBlockingQueue.offer(50);
linkedBlockingQueue.put(100);
System.out.println(linkedBlockingQueue);
System.out.println(linkedBlockingQueue.size());
System.out.println(linkedBlockingQueue.take());
System.out.println(linkedBlockingQueue);
System.out.println(linkedBlockingQueue.poll());
System.out.println(linkedBlockingQueue);
System.out.println(linkedBlockingQueue.peek());
System.out.println(linkedBlockingQueue);
System.out.println(linkedBlockingQueue.remove(50));
System.out.println(linkedBlockingQueue);
}
運行結果:
[15, 60, 50, 100]
4
15
[60, 50, 100]
60
[50, 100]
50
[50, 100]
true
[100]
代碼比較簡單,先試着分析下:
- 創建了一個LinkedBlockingQueue 。
- 分別使用add/offer/put方法向LinkedBlockingQueue中添加元素,其中add方法執行了兩次。
- 打印出LinkedBlockingQueue:[15, 60, 50, 100]。
- 打印出LinkedBlockingQueue的size:4。
- 使用take方法彈出第一個元素,並打印出來:15。
- 打印出LinkedBlockingQueue:[60, 50, 100]。
- 使用poll方法彈出第一個元素,並打印出來:60。
- 打印出LinkedBlockingQueue:[50, 100]。
- 使用peek方法彈出第一個元素,並打印出來:50。
- 打印出LinkedBlockingQueue:[50, 10]。
- 使用remove方法,移除值爲50的元素,返回true。
- 打印出LinkedBlockingQueue:100。
代碼比較簡單,但是還是有些細節不明白:
- 底層是如何保證線程安全性的?
- 數據保存在哪裏,以什麼形式保存的?
- offer/add/put都是往隊列裏面添加元素,區別是什麼?
- poll/take/peek都是彈出隊列的元素,區別是什麼?
要解決上面的疑問,最好的途徑還是看源碼,下面我們就來看看LinkedBlockingQueue的核心源碼。
LinkedBlockingQueue源碼解析
構造方法
LinkedBlockingQueue提供了三個構造方法,如下圖所示:
我們一個一個來分析。
LinkedBlockingQueue()
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
無參的構造方法竟然直接把“鍋”甩出去了,甩給了另外一個構造方法,但是我們要注意傳的參數:Integer.MAX_VALUE。
LinkedBlockingQueue(int capacity)
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
- 判斷傳入的capacity是否合法,如果不大於0,直接拋出異常。
- 把傳入的capacity賦值給capacity。
- 新建一個Node節點,並且把此節點賦值給head和last字段。
這個capacity是什麼呢?如果大家對代碼有一定的感覺的話,應該很容易猜到這是LinkedBlockingQueue的最大容量。如果我們調用無參的構造方法來創建LinkedBlockingQueue的話,那麼它的最大容量就是Integer.MAX_VALUE,我們把它稱爲“無界”,但是我們也可以指定最大容量,那麼此隊列又是一個“有界”隊列了,所以有些博客很草率的說LinkedBlockingQueue是有界隊列,或者是無界隊列,個人認爲這是不嚴謹的。
我們再來看看這個Node是個什麼鬼:
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
是不是有一種莫名的親切感,很明顯,這是單向鏈表的實現呀,next指向的就是下一個Node。
LinkedBlockingQueue(Collection<? extends E> c)
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);//調用第二個構造方法,傳入的capacity是Int的最大值,可以說 是一個無界隊列。
final ReentrantLock putLock = this.putLock;
putLock.lock(); //開啓排他鎖
try {
int n = 0;//用於記錄LinkedBlockingQueue的size
//循環傳入的c集合
for (E e : c) {
if (e == null)//如果e==null,則拋出空指針異常
throw new NullPointerException();
if (n == capacity)//如果n==capacity,說明到了最大的容量,則拋出“Queue full”異常
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));//入隊操作
++n;//n自增
}
count.set(n);//設置count
} finally {
putLock.unlock();//釋放排他鎖
}
}
- 調用第二個構造方法,傳入了int的最大值,所以可以說此時LinkedBlockingQueue是無界隊列。
- 開啓排他鎖putLock 。
- 定義了一個變量n,用來記錄當前LinkedBlockingQueue的size。
- 循環傳入的集合,如果其中的元素爲null,則拋出空指針異常,如果n==capacity,說明到了最大的容量,則拋出“Queue full”異常,否則執行enqueue操作來進行入隊,然後n進行自增。
- 設置count爲n,由此可知,count就是LinkedBlockingQueue的size了。
- 在finally中釋放排他鎖putLock 。
offer
public boolean offer(E e) {
if (e == null) throw new NullPointerException();//如果傳入的元素爲NULL,拋出異常
final AtomicInteger count = this.count;//取出count
if (count.get() == capacity)//如果count==capacity,說明到了最大容量,直接返回false
return false;
int c = -1;//表示size
Node<E> node = new Node<E>(e);//新建Node節點
final ReentrantLock putLock = this.putLock;
putLock.lock();//開啓排他鎖
try {
if (count.get() < capacity) {//如果count<capacity,說明還沒有達到最大容量
enqueue(node);//入隊操作
c = count.getAndIncrement();//獲得count,賦值給c後完成自增操作
if (c + 1 < capacity)//如果c+1 <capacity,說明還有剩餘的空間,喚醒因爲調用notFull的await方法而被阻塞的線程
notFull.signal();
}
} finally {
putLock.unlock();//在finally中釋放排他鎖
}
if (c == 0)//如果c==0,說明釋放putLock的時候,隊列中有一個元素,則調用signalNotEmpty
signalNotEmpty();
return c >= 0;
}
- 如果傳進來的元素爲null,則拋出異常。
- 把本類實例的count賦值給局部變量count。
- 如果count==capacity,說明到了最大的容量,直接返回false。
- 定義局部變量c,用來表示size,初始值是-1。
- 新建Node節點。
- 開啓排他鎖putLock。
- 如果count>=capacity,說明到了最大的容量,釋放排他鎖後,返回false,因爲此時c=-1,c>=0爲false;如果count<capacity,說明還有剩餘空間,繼續往下執行。這裏需要思考一個問題,爲什麼第三步已經判斷過了是否還有剩餘空間,這裏還要再判斷一次呢?因爲可能有多個線程都在執行add/offer/put方法,當隊列沒有滿的時候,多個線程同時執行到第三步(第三步的時候還沒有開啓排他鎖),然後同時往下走,所以開啓排他鎖後,還需要重新判斷下。
- 執行入隊操作。
- 獲得count,並且賦值給c後,完成自增的操作。注意,是先賦值後自增,賦值和自增的先後順序會直接影響到後面的判斷邏輯。
- 如果c+1<capacity,說明還有剩餘的空間,喚醒因爲調用notFull的await方法而被阻塞的線程。這裏爲什麼要+1再進行判斷?因爲在第9步中,是先賦值後自增,也就是說局部變量c保存的還是入隊之前LinkedBlockingQueue的size,所以要先進行+1操作,得到的纔是當前LinkedBlockingQueue的size。
- 在finally中,釋放排他鎖putLock。
- 如果c==0,說明在釋放putLock排他鎖的時候,隊列中有且只有一個元素,則調用signalNotEmpty方法。讓我們來看看signalNotEmpty方法:
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
代碼比較簡單,就是開啓排他鎖,喚醒因爲調用notEmpty的await方法而被阻塞的線程,但是這裏需要注意,這裏獲得的排他鎖已經不再是putLock,而是takeLock。
add
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
add方法直接調用了offer方法,但是add和offer還不完全一樣,當隊列滿了,如果調用offer方法,會直接返回false,但是調用add方法,會拋出"Queue full"的異常。
put
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();//如果傳入的元素爲NULL,拋出異常
int c = -1;//表示size
Node<E> node = new Node<E>(e);//新建Node節點
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;//獲得count
putLock.lockInterruptibly();//開啓排他鎖
try {
//如果到了最大容量,調用notFull的await方法,等待喚醒,用while循環,是爲了防止虛假喚醒
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);//入隊
c = count.getAndIncrement();//count先賦值給c後,再進行自增操作
if (c + 1 < capacity)//如果c+1<capacity,調用notFull的signal方法,喚醒因爲調用notFull的await方法而被阻塞的線程
notFull.signal();
} finally {
putLock.unlock();//釋放排他鎖
}
if (c == 0)//如果隊列中有一個元素,喚醒因爲調用notEmpty的await方法而被阻塞的線程
signalNotEmpty();
}
- 如果傳入的元素爲NULL,則拋出異常。
- 定義一個局部變量c,來表示size,初始值是-1。
- 新建Node節點。
- 把本類實例中的count賦值給局部變量count。
- 開啓排他鎖putLock。
- 如果到了最大容量,則調用notFull的await方法,阻塞當前線程,等待其他線程調用notFull的signal方法來喚醒自己,這裏用while循環是爲了防止虛假喚醒。
- 執行入隊操作。
- count先賦值給c後,再進行自增操作。
- 如果c+1<capacity,說明還有剩餘的空間,則調用notFull的signal方法,喚醒因爲調用notFull的await方法而被阻塞的線程。
- 釋放排他鎖putLock。
- 如果隊列中有且只有一個元素,喚醒因爲調用notEmpty的await方法而被阻塞的線程。
enqueue
private void enqueue(Node<E> node) {
last = last.next = node;
}
入隊操作是不是特別簡單,就是把傳入的Node節點,賦值給last節點的next字段,再賦值給last字段,從而形成一個單向鏈表。
小總結
至此offer/add/put的核心源碼已經分析完畢,我們來做一個小總結,offer/add/put都是添加元素的方法,不過他們之間還是有所區別的,當隊列滿了,調用以上三個方法會出現不同的情況:
- offer:直接返回false。
- add:雖然內部也調用了offer方法,但是隊列滿了,會拋出異常。
- put:線程會阻塞住,等待喚醒。
size
public int size() {
return count.get();
}
沒什麼好說的,count記錄着LinkedBlockingQueue的size,獲得後返回就是了。
take
public E take() throws InterruptedException {
E x;
int c = -1;//size
final AtomicInteger count = this.count;//獲得count
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();//開啓排他鎖
try {
while (count.get() == 0) {//說明目前隊列中沒有數據
notEmpty.await();//阻塞,等待喚醒
}
x = dequeue();//出隊
c = count.getAndDecrement();//先賦值,後自減
if (c > 1)//如果size>1,說明在出隊之前,隊列中有至少兩個元素
notEmpty.signal();//喚醒因爲調用notEmpty的await方法而被阻塞的線程
} finally {
takeLock.unlock();//釋放排他鎖
}
if (c == capacity)//如果隊列中還有一個剩餘空間
signalNotFull();
return x;
}
- 定義局部變量c,用來表示size,初始值是-1。
- 把本類實例的count字段賦值給臨時變量count。
- 開啓響應中斷的排他鎖takeLock 。
- 如果count==0,說明目前隊列中沒有數據,就阻塞當前線程,等待喚醒,直到其他線程調用了notEmpty的signal方法喚醒了當前線程。用while循環是爲了防止虛假喚醒。
- 進行出隊操作。
- count先賦值給c後,在進行自減操作,這裏需要注意是先賦值,後自減。
- 如果c>1,也就是size>1,結合上面的先賦值,後自減,可知如果滿足條件,說明在出隊之前,隊列中至少有兩個元素,則調用notEmpty的signal方法,喚醒因爲調用notEmpty的await方法而被阻塞的線程。
- 釋放排他鎖takeLock 。
- 如果執行出隊後,隊列中有且只有一個剩餘空間,換個說法,就是執行出隊操作前,隊列是滿的,則調用signalNotFull方法。
我們再來看下signalNotFull方法:
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
- 開啓排他鎖,注意這裏的排他鎖是putLock 。
- 調用notFull的signal方法,喚醒因爲調用notFull的await方法而被阻塞的線程。
- 釋放排他鎖putLock 。
poll
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();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
相比take方法,最大的區別就如果隊列爲空,執行take方法會阻塞當前線程,直到被喚醒,而poll方法,直接返回null。
peek
public E peek() {
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
peek方法,只是拿到頭節點的值,但是不會移除該節點。
dequeue
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
沒什麼好說的,就是彈出元素,並且移除彈出的元素。
小總結
至此take/poll/peek的核心源碼已經分析完畢,我們來做一個小總結,take/poll/peek都是獲得頭節點值的方法,不過他們之間還是有所區別的:
- take:當隊列爲空,會阻塞當前線程,直到被喚醒。會進行出隊操作,移除獲得的節點。
- poll:當隊列爲空,直接返回null。會進行出隊操作,移除獲得的節點。
- put:當隊列爲空,直接返回null。不會移除節點。
LinkedBlockingQueue的核心源碼分析到這裏完畢了,謝謝大家。