目錄
1.LinkedBlockingQueue
1.1整體架構
- 主要實現了BlockingQueue和Queue接口
- Queue接口包含了:
遇到隊列滿或空的時候,拋異常,如 add、remove、element;
遇到隊列滿或空的時候,返回特殊值,如 offer、poll、peek。
- BlockingQueue接口包含了:
遇到隊列滿或空的時候,拋異常,如 add、remove、element;
遇到隊列滿或空的時候,返回特殊值/阻塞一段時間如 offer、poll、peek(不會進行阻塞,直接返回);
遇到隊列滿或空的時候,一直阻塞,如 put、take;
- 底層數據結構使用鏈表,鏈表大小可以在初始化進行設置,默認是int的最大值
// 鏈表結構 begin
//鏈表的元素
static class Node<E> {
E item;
//當前元素的下一個,爲空表示當前節點是最後一個
Node<E> next;
Node(E x) { item = x; }
}
//鏈表的容量,默認 Integer.MAX_VALUE
private final int capacity;
//鏈表已有元素大小,使用 AtomicInteger,所以是線程安全的
private final AtomicInteger count = new AtomicInteger();
//鏈表頭
transient Node<E> head;
//鏈表尾
private transient Node<E> last;
// 鏈表結構 end
// 鎖 begin
//take 時的鎖
private final ReentrantLock takeLock = new ReentrantLock();
// take 的條件隊列,condition 可以簡單理解爲基於 ASQ 同步機制建立的條件隊列
private final Condition notEmpty = takeLock.newCondition();
// put 時的鎖,設計兩把鎖的目的,主要爲了 take 和 put 可以同時進行
private final ReentrantLock putLock = new ReentrantLock();
// put 的條件隊列
private final Condition notFull = putLock.newCondition();
// 鎖 end
// 迭代器
// 實現了自己的迭代器
private class Itr implements Iterator<E> {
………………
}
1.2初始化源碼解析
// 不指定容量,默認 Integer 的最大值
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE); //調用指定容量初始化
}
// 指定鏈表容量大小,鏈表頭尾相等,節點值(item)都是 null
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0)
this.capacity = capacity;
// 頭節點和尾節點永遠不爲空,會有一個head節點一直會指向這個哨兵節點!!!
last = head = new Node<E>(null);
}
// 已有集合數據進行初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);//調用指定容量初始化
final ReentrantLock putLock = this.putLock;
putLock.lock(); // 獲取鎖
try {
int n = 0;
for (E e : c) {
// 集合內的元素不能爲空
if (e == null)
throw new NullPointerException();
// capacity 代表鏈表的大小,在這裏是 Integer 的最大值
// 如果集合類的大小大於 Integer 的最大值,就會報錯
// 其實這個判斷完全可以放在 for 循環外面,這樣可以減少 Integer 的最大值次循環(最壞情況)
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
第一種:不指定初始容量默認爲int的最大值
第二種:指定初始值大小,
第三種:指定集合來進行初始化,需要加put鎖來保證線程安全性
ps:三種初始化其實都是指定大小的初始化,只不過進行了封裝,頭節點永遠的會指向一個空值的哨兵節點,可以簡化編程的複雜程度!!!
1.3阻塞新增源碼解析
// 把e新增到隊列的尾部。
public void put(E e) throws InterruptedException {
// e 爲空,拋出異常
if (e == null) throw new NullPointerException();
// 預先設置 c 爲 -1,約定負數爲新增失敗
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock; //獲取鎖
final AtomicInteger count = this.count; //獲取節點值
putLock.lockInterruptibly(); // 可中斷鎖
try {
// 隊列滿了
// 當前線程阻塞,等待其他線程的喚醒(其他線程 take 成功後一定會喚醒此處被阻塞的線程)
while (count.get() == capacity) {
// await 無限等待
notFull.await();
}
// 隊列沒有滿,直接新增到隊列的尾部
enqueue(node);
// 新增計數賦值,注意這裏 getAndIncrement 返回的是舊值
// 這裏的 c 是比真實的 count 小 1 的
c = count.getAndIncrement();
// 如果鏈表現在的大小 小於鏈表的容量,說明隊列未滿
// 可以嘗試喚醒一個 put 的等待線程
if (c + 1 < capacity)
notFull.signal();
} finally {
// 釋放鎖
putLock.unlock();
}
// c==0,代表隊列裏面有一個元素,會嘗試喚醒一個take的等待線程
if (c == 0)
signalNotEmpty();
}
// 入隊,把新元素放到隊尾
private void enqueue(Node<E> node) {
last = last.next = node; //很簡潔,第二個等號賦值後會返回node的值
}
獲取到put鎖,獲取到原子類型count(底層使用的CAS操作),並且設置鎖可中斷,如果隊列已滿就會進行無期限等待(需要手動的進行喚醒)在進行尾部添加節點並且累加count值,如果鏈表沒滿會嘗試喚醒一個put線程,然後釋放鎖。
最後在判斷增加後隊列是否只有一個值,如果是的話需要進行喚醒一個take線程
ps:尾部添加元素由於有了哨兵節點(head節點)變得非常優雅簡介!否則是需要判斷空鏈表,進行更新head節點的。
1.4阻塞刪除源碼分析
// 阻塞拿數據
public E take() throws InterruptedException {
E x;
// 默認負數,代表失敗
int c = -1;
// count 代表當前鏈表數據的真實大小
final AtomicInteger count = this.count;// 獲取原子類型進行操作
final ReentrantLock takeLock = this.takeLock;// 獲取take鎖
takeLock.lockInterruptibly();// 可中斷鎖
try {
// 空隊列時,會被阻塞,等待其他線程喚醒
while (count.get() == 0) {
notEmpty.await();
}
// 非空隊列,從隊列的頭部拿一個出來
x = dequeue();
// 原子操作減一,返回的值是舊值
c = count.getAndDecrement();
// 如果隊列裏面有值,從 take 的等待線程裏面喚醒一個。
if (c > 1)
notEmpty.signal();
} finally {
// 釋放鎖
takeLock.unlock();
}
// 已經刪除了一個元素,
// 如果隊列空閒還剩下一個,嘗試從 put 的等待線程中喚醒一個
if (c == capacity)
signalNotFull();
return x;
}
// 隊頭中取數據
// 每次更新的實際是哨兵節點,是哨兵節點不停地在移動
// 把原本的哨兵節點變爲循環引用,然後更新哨兵爲下一個元素,並將此節點變爲哨兵節點
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;
}
整個過程和put方法大致類似,取頭節點較爲複雜(每次實際爲哨兵節點在不停的移動)
獲取take鎖,獲取原子類型隊列的count,進行加鎖並且設置鎖爲可中斷,如果隊列爲空會無限阻塞(直到有人喚醒),然後從頭節點獲取值並且刪除該節點,如果隊列還有值會嘗試喚醒一個take線程,然後釋放鎖,
如果隊列在取元素之前是滿的那麼會嘗試喚醒一個put線程,最後返回節點的值。
1.5查看元素源碼分析
// 查看並不刪除元素,如果隊列爲空,返回 null
public E peek() {
// count 代表隊列實際大小,隊列爲空,直接返回 null
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();
}
}
只是取元素但不進行刪除,還是會獲取take鎖防止節點被改變,主要就是進行了判斷隊列的空條件,不會被阻塞!