LinkedBlockingQueue原理分析---基於JDK8
1.常用的阻塞佇列
1)ArrayBlockingQueue:規定大小的BlockingQueue,其建構函式必須帶一個int引數來指明其大小.其所含的物件是以FIFO(先入先出)順序排序的.
2)LinkedBlockingQueue:大小不定的BlockingQueue,若其建構函式帶一個規定大小的引數,生成的BlockingQueue有大小限制,若不帶大小引數,所生成的BlockingQueue的大小由Integer.MAX_VALUE來決定.其所含的物件是以FIFO(先入先出)順序排序的
3)PriorityBlockingQueue:類似於LinkedBlockQueue,但其所含物件的排序不是FIFO,而是依據物件的自然排序順序或者是建構函式的Comparator決定的順序.
4)SynchronousQueue:特殊的BlockingQueue,對其的操作必須是放和取交替完成的.
其中LinkedBlockingQueue和ArrayBlockingQueue比較起來,它們背後所用的資料結構不一樣,導致LinkedBlockingQueue的資料吞吐量要大於ArrayBlockingQueue,但線上程數量很大時其效能的可預見性低於ArrayBlockingQueue
2.LinkedBlockingQueue原理
- 基於連結串列實現,執行緒安全的阻塞佇列。
- 使用鎖分離方式提高併發,雙鎖(ReentrantLock):takeLock、putLock,允許讀寫並行,remove(e)和contain()、clear()需要同時獲取2個鎖。
- FIFO先進先出模式。
- 在大部分併發場景下,LinkedBlockingQueue的吞吐量比ArrayBlockingQueue更好,雙鎖,入隊和出隊同時進行
- 根據構造傳入的容量大小決定有界還是無界,預設不傳的話,大小Integer.Max
3.LinkedBlockingQueue的幾個關鍵屬性
static class Node<E> {
E item;
/**後繼節點
*/
Node<E> next;
Node(E x) { item = x; }
}
/** 佇列容量,預設最大,可指定大小 */
private final int capacity;
/** 當前容量 */
private final AtomicInteger count = new AtomicInteger();
/**
* 頭節點.
* Invariant: head.item == null
*/
transient Node<E> head;
/**
* 尾節點.
* Invariant: last.next == null
*/
private transient Node<E> last;
/** 定義的出隊和入隊分離鎖,2個佇列空和滿的出隊和入隊條件 Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
建構函式:預設是佇列,可指定為有界,或初始給於一個初始集合資料
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
* 指定有界大小,同時初始化head和tail節點
*
* @param capacity the capacity of this queue
* @throws IllegalArgumentException if {@code capacity} is not greater
* than zero
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
/**
* 遍歷集合元素,放到佇列進行初始化 --- 無界佇列
*
* @param c the collection of elements to initially contain
* @throws NullPointerException if the specified collection or any
* of its elements are 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();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
4.BlockingQueue原始碼分析
//入隊,將元素新增到對尾等價 last.next = node; last = last.next
private void enqueue(Node<E> node) {
last = last.next = node;
}
/**
* 出隊,從頭部出
*
* @return the node
*/
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
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;
}
// 佇列已滿:false
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity) // 佇列容量達到最大值,新增失敗
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // 獲取插入鎖putLock
try {
if (count.get() < capacity) { // 加鎖後再次判斷佇列是否已滿
enqueue(node); // 入隊
c = count.getAndIncrement(); // 返回Inc之前的值
if (c + 1 < capacity) // 插入節點後佇列未滿
notFull.signal(); // 喚醒notFull上的等待執行緒
}
} finally {
putLock.unlock(); // 釋放插入鎖
}
if (c == 0)
signalNotEmpty(); // 如果offer前佇列為空,則喚醒notEmpty上的等待執行緒
return c >= 0;
}
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException 方法和offer(E e)程式碼和功能均相似,但是如果在指定時間內未插入成功則會返回false。
比offer(E e)多的部分程式碼分析:
long nanos = unit.toNanos(timeout); //將指定的時間長度轉換為毫秒來進行處理
while (count.get() == capacity) {
if (nanos <= 0) // 等待的剩餘時間小於等於0,那麼直接返回false
return false;
nanos = notFull.awaitNanos(nanos); // 最多等待時間(納秒)
}
//插入節點:\n執行緒入隊操作前會獲取putLock鎖,插入資料完畢後釋放;
佇列未滿將新建Node節點,新增到佇列末尾;
佇列已滿則阻塞執行緒(notFull.await())或返回false;若執行緒B取出資料,則會呼叫notFull.signal()喚醒notFull上的等待執行緒(執行緒A繼續插資料)。
若入隊前佇列為空,則喚醒notEmpty上等待的獲取資料的執行緒
// 一直阻塞直到插入成功
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;
// 可中斷的鎖獲取操作(優先考慮響應中斷),如果執行緒由於獲取鎖而處於Blocked狀態時,執行緒將被中斷而不再繼續等待(throws InterruptedException),可避免死鎖。
putLock.lockInterruptibly();
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迴圈可避免“偽喚醒”(執行緒被喚醒時佇列大小依舊達到最大值)
while (count.get() == capacity) {
notFull.await(); // notFull:入隊條件
}
enqueue(node); // 將node連結到佇列尾部
c = count.getAndIncrement(); // 元素入隊後佇列元素總和
if (c + 1 < capacity) // 佇列未滿
notFull.signal(); // 喚醒其他執行入佇列的執行緒
} finally {
putLock.unlock(); // 釋放鎖
}
// c=0說明佇列之前為空,出佇列執行緒均處於等待狀態。新增一個元素後,佇列已不為空,於是喚醒等待獲取元素的執行緒
if (c == 0)
signalNotEmpty();
}
獲取方法
先看幾個重要方法:
- /**
- * 喚醒等待插入資料的執行緒. Called only from take/poll.
- */
- private void signalNotFull() {
- final ReentrantLock putLock = this.putLock;
- putLock.lock();
- try {
- notFull.signal();
- } finally {
- putLock.unlock();
- }
- }
- /**
- * 佇列頭部元素出隊.
- *
- * @return the node
- */
- private E dequeue() {
- // assert takeLock.isHeldByCurrentThread();
- // assert head.item == null;
- Node<E> h = head; // 臨時變數h
- Node<E> first = h.next;
- h.next = h; // 形成環引用help GC
- head = first;
- E x = first.item;
- first.item = null;
- return x;
- }
4.1、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(); // 減1並返回舊值
- if (c > 1)
- notEmpty.signal(); // 喚醒其他取資料的執行緒
- }
- } finally {
- takeLock.unlock();
- }
- // c等於capacity說明poll之前佇列已滿,poll一個元素後便可喚醒其他等待插入資料的執行緒
- if (c == capacity)
- signalNotFull();
- return x;
- }
衍生方法:
// 為poll方法增加了時間限制,指定時間未取回資料則返回null
- public E poll(long timeout, TimeUnit unit)throws InterruptedException{}
4.2、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) // take資料前佇列大小大於1,則take後佇列至少還有1個元素
- notEmpty.signal(); // 喚醒其他取資料的執行緒
- } finally {
- takeLock.unlock();
- }
- if (c == capacity)
- signalNotFull(); //喚醒其他等待插入資料的執行緒
- return x;
- }
4.3、drainTo(Collection<? super E> c, int maxElements)
// 移除最多maxElements個元素並將其加入集合
- public int drainTo(Collection<? super E> c, int maxElements) {
- if (c == null)
- throw new NullPointerException();
- if (c == this)
- throw new IllegalArgumentException();
- if (maxElements <= 0)
- return 0;
- boolean signalNotFull = false;
- final ReentrantLock takeLock = this.takeLock;
- takeLock.lock();
- try {
- int n = Math.min(maxElements, count.get());//轉移元素數量不能超過佇列總量
- // count.get provides visibility to first n Nodes
- Node<E> h = head;
- int i = 0;
- try {
- while (i < n) {
- Node<E> p = h.next;//從隊首獲取元素
- c.add(p.item);
- p.item = null;//p為臨時變數,置null方便GC
- h.next = h;
- h = p;
- ++i;
- }
- return n;
- } finally {
- // Restore invariants even if c.add() threw
- if (i > 0) { // 有資料被轉移到集合c中
- // assert h.item == null;
- head = h;
- //如果轉移前的佇列大小等於佇列容量,則說明現在佇列未滿
- // 更新count為佇列實際大小(減去i得到)
- signalNotFull = (count.getAndAdd(-i) == capacity);
- }
- }
- } finally {
- takeLock.unlock();
- if (signalNotFull)
- signalNotFull(); // 喚醒其他等待插入資料的執行緒
- }
- }
衍生方法:
// 將[所有]可用元素加入集合c
- public int drainTo(Collection<? super E> c) {
- return drainTo(c, Integer.MAX_VALUE);
- }
4.4、boolean retainAll(Collection<?> c)
// 僅保留集合c中包含的元素,佇列因此請求而改變則返回true
- public boolean retainAll(Collection<?> c) {
- Objects.requireNonNull(c); // 集合為null則throw NPE
- boolean modified = false;
- Iterator<E> it = iterator();
- while (it.hasNext()) {
- if (!c.contains(it.next())) {
- it.remove();
- modified = true; // 佇列因此請求而改變則返回true
- }
- }
- return modified;
- }
LinkedBlockingQueue取資料小結:
執行緒A取資料前會獲取takeLock鎖,取完資料後釋放鎖。
佇列有資料則(通常)返回隊首資料;
若佇列為空,則阻塞執行緒(notEmpty.await())或返回null等;當執行緒B插入資料後,會呼叫notEmpty.signal()喚醒notEmpty上的等待執行緒(執行緒A繼續取資料)。
若取資料前佇列已滿,則通過notFull.signal()喚醒notFull上等待插入資料的執行緒。
5、檢測方法(取回但不移除)
5.1、E peek()
// 返回佇列頭,佇列為空返回null
- 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();
- }
- }
6、綜述
6.1、LinkedBlockingQueue通過對 插入、取出資料 使用不同的鎖,實現多執行緒對競爭資源的互斥訪問;
6.2、(之前佇列為空)新增資料後呼叫signalNotEmpty()方法喚醒等待取資料的執行緒;(之前佇列已滿)取資料後呼叫signalNotFull()喚醒等待插入資料的執行緒。這種喚醒模式可節省執行緒等待時間。
6.3、個別操作需要呼叫方法fullyLock()同時獲取putLock、takeLock兩把鎖(如方法:clear()、contains(Object o)、remove(Object o)、toArray()、toArray(T[] a)、toString()),注意fullyLock和fullyUnlock獲取鎖和解鎖的順序剛好相反,避免死鎖。
- /**
- * Locks to prevent both puts and takes.
- */
- void fullyLock() {
- putLock.lock();
- takeLock.lock();
- }
- /**
- * Unlocks to allow both puts and takes.
- */
- void fullyUnlock() {
- takeLock.unlock();
- putLock.unlock();
- }
6.4、執行緒喚醒signal()
值得注意的是,對notEmpty和notFull的喚醒操作均使用的是signal()而不是signalAll()。
signalAll() 雖然能喚醒Condition上所有等待的執行緒,但卻並不見得會節省資源,相反,喚醒操作會帶來上下文切換,且會有鎖的競爭。此外,由於此處獲取的鎖均是同一個(putLock或takeLock),同一時刻被鎖的執行緒只有一個,也就無從談起喚醒多個執行緒了。
6.5、LinkedBlockingQueue與ArrayBlockingQueue簡要比較
ArrayBlockingQueue底層基於陣列,建立時必須指定佇列大小,“有界”;LinkedBlockingQueue“無界”,節點動態建立,節點出隊後可被GC,故伸縮性較好;
ArrayBlockingQueue入隊和出隊使用同一個lock(但資料讀寫操作已非常簡潔),讀取和寫入操作無法並行,LinkedBlockingQueue使用雙鎖可並行讀寫,其吞吐量更高。
ArrayBlockingQueue在插入或刪除元素時直接放入陣列指定位置(putIndex、takeIndex),不會產生或銷燬任何額外的物件例項;而LinkedBlockingQueue則會生成一個額外的Node物件,在高效併發處理大量資料時,對GC的影響存在一定的區別。
參考、感謝:
http://blog.csdn.net/u010887744/article/details/73010691
相關文章
- PriorityQueue原理分析——基於原始碼原始碼
- 基於Apache元件,分析物件池原理Apache元件物件
- 基於LinkedBlockingQueue實現股票交易系統BloC
- 基於JDK的動態代理原理分析JDK
- 基於HiKariCP元件,分析連線池原理元件
- Java容器類原始碼分析之Iterator與ListIterator迭代器(基於JDK8)Java原始碼JDK
- 基於Lucene查詢原理分析Elasticsearch的效能Elasticsearch
- LinkedBlockingQueue中put原始碼分析BloC原始碼
- Java LinkedBlockingQueue和ArrayBlockingQueue分析JavaBloC
- Android外掛化原理分析(基於Neptune框架)Android框架
- 通訊原理:基於MATLAB的AM調幅分析Matlab
- HashMap原始碼分析(JDK8)HashMap原始碼JDK
- 探究synchronized底層原理(基於JAVA8原始碼分析)synchronizedJava原始碼
- 基於 Formily 的表單設計器實現原理分析 ORM
- JDK8基礎映象包JDK
- ArrayBlockingQueue 和 LinkedBlockingQueue 效能測試與分析BloC
- 基於page-skeleton-webpack-plugin分析自動生成骨架屏原理WebPlugin
- Kafka原理分析之基礎篇Kafka
- 【JUC】JDK1.8原始碼分析之LinkedBlockingQueue(四)JDK原始碼BloC
- 資料庫持久化中的讀寫效能原理分析---基於儲存引擎和索引原理資料庫持久化儲存引擎索引
- 基於RPC原理的dubboRPC
- 迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析JavaBloC佇列
- 併發容器之ArrayBlockingQueue和LinkedBlockingQueue實現原理詳解BloC
- web DB 公共方法原理基於sqlLiteWebSQL
- 死磕java concurrent包系列(五)基於AQS的條件佇列把LinkedBlockingQueue“扒光”JavaAQS佇列BloC
- 應用配置管理,基礎原理分析
- 基於 Android 讀取微信本地 DB 資料 | 思維原理及技術分析Android
- 動態代理是基於什麼原理?
- 基於mysql分散式悲觀鎖原理MySql分散式
- 基於DEM的坡度坡向分析
- 基於Python的效能分析Python
- 阻塞佇列--LinkedBlockingQueue佇列BloC
- Java JUC LinkedBlockingQueue解析JavaBloC
- LinkedBlockingQueue 原始碼解析BloC原始碼
- 服務管理與通訊,基礎原理分析
- HashSet的add的實現原理深入刨析[jdk8]JDK
- Go編譯原理系列2(詞法分析&語法分析基礎)Go編譯原理詞法分析語法分析
- jdk8JDK