LinkedBlockingQueue源碼解析
隊列是很重要的API,線程池、讀寫鎖、消息隊列等技術和框架的底層原理都是隊列,是很多高級API的基礎。
LinkedBlockingQueue可以理解爲一個典型的生產者-消費者模型。
1.整體架構
LinkedBlockingQueue,鏈表阻塞隊列,底層的數據結構時鏈表,且隊列是可阻塞的。
Queue接口和BlockingQueue接口
從類關係圖中可以看出兩條線路,
線路1
AbstractQueue --> AbstractCollection --> Collection --> Iterable
這條路線主要是想複用Collection和迭代器的一些操作。
線路2
BlockingQueue --> Queue -->Collection
Queue是最基礎的接口,幾乎所有隊列實現類都實現這個接口,該接口定義了隊列的三類基本操作,
- 新增操作
add方法隊列滿時拋出異常
offer方法隊列滿時返回false - 查看並刪除操作
remove方法隊列爲空時返回false
poll方法隊列爲空時返回null - 只查看但不刪除操作
element方法隊列爲空時拋出異常
peek方法隊列爲空時返回null
BlockingQueue在Queue的基礎上添加了阻塞的概念,如一直阻塞或阻塞指定時間,
操作類型 | 拋異常 | 特殊值 | 阻塞 | 阻塞一段時間 |
---|---|---|---|---|
新增直至隊列已滿 | add | offer返回false | put | offer設置超時,超時返回false |
彈出隊列頭至隊列爲空 | 無 | remove返回false;poll返回null | take | poll設置超時,超時返回null |
查看隊列頭但不彈出 | element | peek返回null | 無 | 無 |
類註釋
- 基於鏈表的阻塞隊列,底層數據結構是鏈表
- 鏈表維護先入先出隊列,新元素添加到隊尾,獲取元素時從頭部取出
- 鏈表的大小在初始化時候可以設置,默認是Integer的最大值
- 可以使用Collection和Iterable的所有操作
內部結構
LinkedBlockingQueue可以分爲三個部分,存儲鏈表+鎖+迭代器,
// 鏈表節點
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
// 鏈表容量,默認情況下是 Integer.MAX_VALUE
private final int capacity;
// 使用原子性的Integer類對象記錄鏈表元素數目
private final AtomicInteger count = new AtomicInteger();
// 鏈表頭
transient Node<E> head;
// 鏈表尾
private transient Node<E> last;
// 獲取元素時候的鎖,take和poll方法會用到
private final ReentrantLock takeLock = new ReentrantLock();
// 獲取元素時的條件隊列,可以理解爲 等待鏈表not empty時才獲取元素
private final Condition notEmpty = takeLock.newCondition();
// 添加元素時候的鎖,put和offer方法會用到
private final ReentrantLock putLock = new ReentrantLock();
// 添加元素時的條件隊列,可以理解爲 等待鏈表not Full時才添加元素
private final Condition notFull = putLock.newCondition();
// 內部實現的迭代器
private class Itr implements Iterator<E> {
......
}
三種結構各司其職,
- 鏈表作用是保存當前節點,節點使用了泛型,所以節點中的數據可以是任意的對象。
- 鎖有take和put鎖,目的是保證隊列操作時線程安全,同時take和put操作可以同時進行,互不影響。
初始化
LinkedBlockingQueue有三種初始化方式,
// 無參數初始化,默認容量是Integer.MAX_VALUE
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); // 注意,head節點一定是固定的值爲null的節點對象,保證了不會空指針異常
}
// 使用集合對象初始化
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(); // 集合類元素不能爲null
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
初始化源碼中包含的信息,
- 初始化時容量大小不影響性能,隻影響以後的使用,初始化隊列太小會導致過早報出IllegalStateException異常
- 源碼中for循環的形式並不優雅,添加一個元素後檢查是否超過capacity是一種低效的方式。完全可以先得到集合對象的size,直接判斷是否與設定的capacity衝突。
2.源碼解析
入隊和出隊操作
隊列是先進先出的結構,入隊元素會被添加到隊尾,出隊元素是隊列頭部元素。分別對應於enqueue和dequeue方法。
1) enqueue方法
入隊方法很簡單,在隊尾添加節點後將last指針指向新加入的節點對象。
private void enqueue(Node<E> node) {
last = last.next = node;
}
初始化方法中,last指針首先會知道一個item爲null的節點對象上,因此在添加節點的過程中不會出現空指針異常。
2) dequeue方法
出隊每次需要將隊列頭部元素取出,注意需要保證head指針始終指向的是item爲null的節點。
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next; // 此時h是初始化時創建的item爲null的節點,first指向的是實際的隊列頭部
h.next = h; // help GC
head = first; // head指針指向隊列頭部節點
E x = first.item;
first.item = null; // 返回頭部節點的item值,並將頭部節點的item設置爲null,成爲新的head節點
return x;
}
新增節點操作
1) add方法
add方法在容量達到capacity時會拋出異常,
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
可見該方法底層調用的是offer方法,通過offer方法的返回值判斷是否添加成功,如果添加失敗則會拋出異常。
2) offer方法
offer方法不會拋出異常,而是在添加成功後返回true,反之,返回false。
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; // 注意這裏 c初始化值爲是-1
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock; // 爭取到put鎖後,上鎖
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); // 如果隊列未滿,添加元素後count值值賦予c變量,之後增加
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty(); // 如果c從-1到0說明是第一次加入元素,隊列從空變爲非空,喚醒put等待隊列中的線程
return c >= 0;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
3) 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();
try {
// 如果隊列已滿則阻塞,可簡單記憶爲 wait not full,等待未滿的過程
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); // 添加後如果還未滿,可嘗試喚醒另一個put等待隊列中的對象
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
三種添加元素的方法進行如下總結,
- 添加元素的第一步是上鎖,保證線程安全
- 新增數據直接添加到隊尾即可
- 新增數據時,如果容量滿了,則當前線程阻塞,直至隊頭元素被取出使得隊列中存在空位。阻塞是通過鎖實現的,具體原理也是等待隊列,會在以後鎖相關的筆記中說明
- 添加元素成功後,如果隊列未滿,會嘗試喚醒putLock的等待線程;同時,如果隊列不爲空,會喚醒takeLock的等待線程。保證一旦滿足put或take的條件,就能夠喚起等待線程,不會浪費時間。
offer方法可以設置阻塞一定時間,具體原理與put方法相同,只是在一定時間範圍內阻塞,
刪除節點操作
隊列的刪除節點操作返回的是隊列頭節點的值,但是具體返回形式有兩種,一種是返回值的同時刪除頭節點,另一種是返回值但是不刪除節點。
刪除數據關注兩點,
- 刪除原理
- 查看並刪除和查看不刪除在實現方式上的區別
查看並刪除
1) 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) { // 如果隊列爲空,則阻塞,wait not empty
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
- 先上鎖
- 如果隊列爲空,則阻塞,直至隊列非空;反之,使用dequeue方法刪除隊頭節點,並返回隊頭節點的值
- 如果滿足put或take的條件,會嘗試喚醒等待隊列中的線程
2) poll方法(非阻塞)
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();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
與take方法相比,不存在阻塞過程。
查看但不刪除—peek方法
public E peek() {
if (count.get() == 0) // 隊列爲空則返回null
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item; // 獲取隊頭節點的item值並返回
} finally {
takeLock.unlock();
}
}
該過程不涉及使用dequeue刪除對頭節點。
總結
LinkedBlockingQueue可以應用到多線程環境中,如消費者-生產者模型。隊列本身也是很重要的數據結構。