解讀 Java 併發隊列 BlockingQueue
轉自:https://javadoop.com/post/java-concurrent-queue
最近得空,想寫篇文章好好說說 java 線程池問題,我相信很多人都一知半解的,包括我自己在仔仔細細看源碼之前,也有許多的不解,甚至有些地方我一直都沒有理解到位。
說到線程池實現,那麼就不得不涉及到各種 BlockingQueue 的實現,那麼我想就 BlockingQueue 的問題和大家分享分享我瞭解的一些知識。
本文沒有像之前分析 AQS 那樣一行一行源碼分析了,不過還是把其中最重要和最難理解的代碼說了一遍,所以不免篇幅略長。本文涉及到比較多的 Doug Lea 對 BlockingQueue 的設計思想,希望有心的讀者真的可以有一些收穫,我覺得自己還是寫了一些乾貨的。
本文直接參考 Doug Lea 寫的 Java doc 和註釋,這也是我們在學習 java 併發包時最好的材料了。希望大家能有所思、有所悟,學習 Doug Lea 的代碼風格,並將其優雅、嚴謹的作風應用到我們寫的每一行代碼中。
目錄
阻塞隊列概覽
Java中的阻塞隊列
阻塞隊列概覽
1. 什麼是阻塞隊列?
阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作是:在隊列爲空時,獲取元素的線程會等待隊列變爲非空。當隊列滿時,存儲元素的線程會等待隊列可用。阻塞隊列常用於生產者和消費者的場景,生產者是往隊列裏添加元素的線程,消費者是從隊列裏拿元素的線程。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器裏拿元素。
阻塞隊列提供了四種處理方法:
方法\處理方式 | 拋出異常 | 返回特殊值 | 一直阻塞 | 超時退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
檢查方法 | element() | peek() | 不可用 | 不可用 |
拋出異常:是指當阻塞隊列滿時候,再往隊列裏插入元素,會拋出IllegalStateException(“Queue full”)異常。當隊列爲空時,從隊列裏獲取元素時會拋出NoSuchElementException異常 。
返回特殊值:插入方法會返回是否成功,成功則返回true。移除方法,則是從隊列裏拿出一個元素,如果沒有則返回null
一直阻塞:當阻塞隊列滿時,如果生產者線程往隊列裏put元素,隊列會一直阻塞生產者線程,直到拿到數據,或者響應中斷退出。當隊列空時,消費者線程試圖從隊列裏take元素,隊列也會阻塞消費者線程,直到隊列可用。
超時退出:當阻塞隊列滿時,隊列會阻塞生產者線程一段時間,如果超過一定的時間,生產者線程就會退出。
2、Java裏的阻塞隊列
JDK7提供了7個阻塞隊列。分別是
ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。
LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。
PriorityBlockingQueue :一個支持優先級排序的×××阻塞隊列。
DelayQueue:一個使用優先級隊列實現的×××阻塞隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。
LinkedTransferQueue:一個由鏈表結構組成的×××阻塞隊列。
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
ArrayBlockingQueue
ArrayBlockingQueue是一個用數組實現的有界阻塞隊列。此隊列按照先進先出(FIFO)的原則對元素進行排序。默認情況下不保證訪問者公平的訪問隊列,所謂公平訪問隊列是指阻塞的所有生產者線程或消費者線程,當隊列可用時,可以按照阻塞的先後順序訪問隊列,即先阻塞的生產者線程,可以先往隊列裏插入元素,先阻塞的消費者線程,可以先從隊列裏獲取元素。通常情況下爲了保證公平性會降低吞吐量。
LinkedBlockingQueue
LinkedBlockingQueue是一個用鏈表實現的有界阻塞隊列。此隊列的默認和最大長度爲Integer.MAX_VALUE。此隊列按照先進先出的原則對元素進行排序。
PriorityBlockingQueue
PriorityBlockingQueue是一個支持優先級的×××隊列。默認情況下元素採取自然順序排列,也可以通過比較器comparator來指定元素的排序規則。元素按照升序排列。
DelayQueue
DelayQueue是一個支持延時獲取元素的×××阻塞隊列。隊列使用PriorityQueue來實現。隊列中的元素必須實現Delayed接口,在創建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素。我們可以將DelayQueue運用在以下應用場景:
緩存系統的設計:可以用DelayQueue保存緩存元素的有效期,使用一個線程循環查詢DelayQueue,一旦能從DelayQueue中獲取元素時,表示緩存有效期到了。
定時任務調度。使用DelayQueue保存當天將會執行的任務和執行時間,一旦從DelayQueue中獲取到任務就開始執行,從比如TimerQueue就是使用DelayQueue實現的。
3、阻塞隊列源碼分析:
BlockingQueue
首先,最基本的來說, BlockingQueue 是一個先進先出的隊列(Queue),爲什麼說是阻塞(Blocking)的呢?是因爲 BlockingQueue 支持當獲取隊列元素但是隊列爲空時,會阻塞等待隊列中有元素再返回;也支持添加元素時,如果隊列已滿,那麼等到隊列可以放入新元素時再放入。
BlockingQueue 是一個接口,繼承自 Queue,所以其實現類也可以作爲 Queue 的實現來使用,而 Queue 又繼承自 Collection 接口。
BlockingQueue 對插入操作、移除操作、獲取元素操作提供了四種不同的方法用於不同的場景中使用:1、拋出異常;2、返回特殊值(null 或 true/false,取決於具體的操作);3、阻塞等待此操作,直到這個操作成功;4、阻塞等待此操作,直到成功或者超時指定時間。總結如下:
Throws exception | Special value | Blocks | Times out | |
---|---|---|---|---|
Insert | add(e) | offer(e) | put(e) | offer(e, time, unit) |
Remove | remove() | poll() | take() | poll(time, unit) |
Examine | element() | peek() | not applicable | not applicable |
BlockingQueue 的各個實現都遵循了這些規則,當然我們也不用死記這個表格,知道有這麼回事,然後寫代碼的時候根據自己的需要去看方法的註釋來選取合適的方法即可。
對於 BlockingQueue,我們的關注點應該在 put(e) 和 take() 這兩個方法,因爲這兩個方法是帶阻塞的。
BlockingQueue 不接受 null 值的插入,相應的方法在碰到 null 的插入時會拋出 NullPointerException 異常。null 值在這裏通常用於作爲特殊值返回(表格中的第三列),代表 poll 失敗。所以,如果允許插入 null 值的話,那獲取的時候,就不能很好地用 null 來判斷到底是代表失敗,還是獲取的值就是 null 值。
一個 BlockingQueue 可能是有界的,如果在插入的時候,發現隊列滿了,那麼 put 操作將會阻塞。通常,在這裏我們說的×××隊列也不是說真正的×××,而是它的容量是 Integer.MAX_VALUE(21億多)。
BlockingQueue 是設計用來實現生產者-消費者隊列的,當然,你也可以將它當做普通的 Collection 來用,前面說了,它實現了 java.util.Collection 接口。例如,我們可以用 remove(x) 來刪除任意一個元素,但是,這類操作通常並不高效,所以儘量只在少數的場合使用,比如一條消息已經入隊,但是需要做取消操作的時候。
BlockingQueue 的實現都是線程安全的,但是批量的集合操作如 addAll
, containsAll
, retainAll
和 removeAll
不一定是原子操作。如 addAll(c) 有可能在添加了一些元素後中途拋出異常,此時 BlockingQueue 中已經添加了部分元素,這個是允許的,取決於具體的實現。
BlockingQueue 不支持 close 或 shutdown 等關閉操作,因爲開發者可能希望不會有新的元素添加進去,此特性取決於具體的實現,不做強制約束。
最後,BlockingQueue 在生產者-消費者的場景中,是支持多消費者和多生產者的,說的其實就是線程安全問題。
相信上面說的每一句都很清楚了,BlockingQueue 是一個比較簡單的線程安全容器,下面我會分析其具體的在 JDK 中的實現,這裏又到了 Doug Lea 表演時間了。
BlockingQueue 實現之 ArrayBlockingQueue
ArrayBlockingQueue 是 BlockingQueue 接口的有界隊列實現類,底層採用數組來實現。
其併發控制採用可重入鎖來控制,不管是插入操作還是讀取操作,都需要獲取到鎖才能進行操作。
如果讀者看過我之前寫的《一行一行源碼分析清楚 AbstractQueuedSynchronizer(二)》 的關於 Condition 的文章的話,那麼你一定能很容易看懂 ArrayBlockingQueue 的源碼,它採用一個 ReentrantLock 和相應的兩個 Condition 來實現。
ArrayBlockingQueue 共有以下幾個屬性:
// 用於存放元素的數組final Object[] items;// 下一次讀取操作的位置int takeIndex;// 下一次寫入操作的位置int putIndex;// 隊列中的元素數量int count;// 以下幾個就是控制併發用的同步器final ReentrantLock lock;private final Condition notEmpty;private final Condition notFull;
我們用個示意圖來描述其同步機制:
ArrayBlockingQueue 實現併發同步的原理就是,讀操作和寫操作都需要獲取到 AQS 獨佔鎖才能進行操作。如果隊列爲空,這個時候讀操作的線程進入到讀線程隊列排隊,等待寫線程寫入新的元素,然後喚醒讀線程隊列的第一個等待線程。如果隊列已滿,這個時候寫操作的線程進入到寫線程隊列排隊,等待讀線程將隊列元素移除騰出空間,然後喚醒寫線程隊列的第一個等待線程。
對於 ArrayBlockingQueue,我們可以在構造的時候指定以下三個參數:
隊列容量,其限制了隊列中最多允許的元素個數;
指定獨佔鎖是公平鎖還是非公平鎖。非公平鎖的吞吐量比較高,公平鎖可以保證每次都是等待最久的線程獲取到鎖;
可以指定用一個集合來初始化,將此集合中的元素在構造方法期間就先添加到隊列中。
更具體的源碼我就不進行分析了,因爲它就是 AbstractQueuedSynchronizer 中 Condition 的使用,感興趣的讀者請看我寫的《一行一行源碼分析清楚 AbstractQueuedSynchronizer(二)》,因爲只要看懂了那篇文章,ArrayBlockingQueue 的代碼就沒有分析的必要了,當然,如果你完全不懂 Condition,那麼基本上也就可以說看不懂 ArrayBlockingQueue 的源碼了。
BlockingQueue 實現之 LinkedBlockingQueue
底層基於單向鏈表實現的阻塞隊列,可以當做×××隊列也可以當做有界隊列來使用。看構造方法:
// 傳說中的×××隊列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); }
我們看看這個類有哪些屬性:
// 隊列容量private final int capacity;// 隊列中的元素數量private final AtomicInteger count = new AtomicInteger(0);// 隊頭private transient Node<E> head;// 隊尾private transient Node<E> last;// take, poll, peek 等讀操作的方法需要獲取到這個鎖private final ReentrantLock takeLock = new ReentrantLock();// 如果讀操作的時候隊列是空的,那麼等待 notEmpty 條件private final Condition notEmpty = takeLock.newCondition();// put, offer 等寫操作的方法需要獲取到這個鎖private final ReentrantLock putLock = new ReentrantLock();// 如果寫操作的時候隊列是滿的,那麼等待 notFull 條件private final Condition notFull = putLock.newCondition();
這裏用了兩個鎖,兩個 Condition,簡單介紹如下:
takeLock 和 notEmpty 怎麼搭配:如果要獲取(take)一個元素,需要獲取 takeLock 鎖,但是獲取了鎖還不夠,如果隊列此時爲空,還需要隊列不爲空(notEmpty)這個條件(Condition)。
putLock 需要和 notFull 搭配:如果要插入(put)一個元素,需要獲取 putLock 鎖,但是獲取了鎖還不夠,如果隊列此時已滿,還需要隊列不是滿的(notFull)這個條件(Condition)。
首先,這裏用一個示意圖來看看 LinkedBlockingQueue 的併發讀寫控制,然後再開始分析源碼:
看懂這個示意圖,源碼也就簡單了,讀操作是排好隊的,寫操作也是排好隊的,唯一的併發問題在於一個寫操作和一個讀操作同時進行,只要控制好這個就可以了。
先上構造方法:
public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); }
注意,這裏會初始化一個空的頭結點,那麼第一個元素入隊的時候,隊列中就會有兩個元素。讀取元素時,也總是獲取頭節點後面的一個節點。count 的計數值不包括這個頭節點。
我們來看下 put 方法是怎麼將元素插入到隊尾的:
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // 如果你糾結這裏爲什麼是 -1,可以看看 offer 方法。這就是個標識成功、失敗的標誌而已。 int c = -1; Node<E> node = new Node(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; // 必須要獲取到 putLock 纔可以進行插入操作 putLock.lockInterruptibly(); try { // 如果隊列滿,等待 notFull 的條件滿足。 while (count.get() == capacity) { notFull.await(); } // 入隊 enqueue(node); // count 原子加 1,c 還是加 1 前的值 c = count.getAndIncrement(); // 如果這個元素入隊後,還有至少一個槽可以使用,調用 notFull.signal() 喚醒等待線程。 // 哪些線程會等待在 notFull 這個 Condition 上呢? if (c + 1 < capacity) notFull.signal(); } finally { // 入隊後,釋放掉 putLock putLock.unlock(); } // 如果 c == 0,那麼代表隊列在這個元素入隊前是空的(不包括head空節點), // 那麼所有的讀線程都在等待 notEmpty 這個條件,等待喚醒,這裏做一次喚醒操作 if (c == 0) signalNotEmpty(); }// 入隊的代碼非常簡單,就是將 last 屬性指向這個新元素,並且讓原隊尾的 next 指向這個元素// 這裏入隊沒有併發問題,因爲只有獲取到 putLock 獨佔鎖以後,纔可以進行此操作private void enqueue(Node<E> node) { // assert putLock.isHeldByCurrentThread(); // assert last.next == null; last = last.next = node; }// 元素入隊後,如果需要,調用這個方法喚醒讀線程來讀private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { notEmpty.signal(); } finally { takeLock.unlock(); } }
我們再看看 take 方法:
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; // 首先,需要獲取到 takeLock 才能進行出隊操作 takeLock.lockInterruptibly(); try { // 如果隊列爲空,等待 notEmpty 這個條件滿足再繼續執行 while (count.get() == 0) { notEmpty.await(); } // 出隊 x = dequeue(); // count 進行原子減 1 c = count.getAndDecrement(); // 如果這次出隊後,隊列中至少還有一個元素,那麼調用 notEmpty.signal() 喚醒其他的讀線程 if (c > 1) notEmpty.signal(); } finally { // 出隊後釋放掉 takeLock takeLock.unlock(); } // 如果 c == capacity,那麼說明在這個 take 方法發生的時候,隊列是滿的 // 既然出隊了一個,那麼意味着隊列不滿了,喚醒寫線程去寫 if (c == capacity) signalNotFull(); return x; }// 取隊頭,出隊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; }// 元素出隊後,如果需要,調用這個方法喚醒寫線程來寫private void signalNotFull() { final ReentrantLock putLock = this.putLock; putLock.lock(); try { notFull.signal(); } finally { putLock.unlock(); } }
源碼分析就到這裏結束了吧,畢竟還是比較簡單的源碼,基本上只要讀者認真點都看得懂。
BlockingQueue 實現之 SynchronousQueue
它是一個特殊的隊列,它的名字其實就蘊含了它的特徵 - - 同步的隊列。爲什麼說是同步的呢?這裏說的並不是多線程的併發問題,而是因爲當一個線程往隊列中寫入一個元素時,寫入操作不會立即返回,需要等待另一個線程來將這個元素拿走;同理,當一個讀線程做讀操作的時候,同樣需要一個相匹配的寫線程的寫操作。這裏的 Synchronous 指的就是讀線程和寫線程需要同步,一個讀線程匹配一個寫線程。
我們比較少使用到 SynchronousQueue 這個類,不過它在線程池的實現類 ScheduledThreadPoolExecutor 中得到了應用,感興趣的讀者可以在看完這個後去看看相應的使用。
雖然上面我說了隊列,但是 SynchronousQueue 的隊列其實是虛的,其不提供任何空間(一個都沒有)來存儲元素。數據必須從某個寫線程交給某個讀線程,而不是寫到某個隊列中等待被消費。
你不能在 SynchronousQueue 中使用 peek 方法(在這裏這個方法直接返回 null),peek 方法的語義是隻讀取不移除,顯然,這個方法的語義是不符合 SynchronousQueue 的特徵的。SynchronousQueue 也不能被迭代,因爲根本就沒有元素可以拿來迭代的。雖然 SynchronousQueue 間接地實現了 Collection 接口,但是如果你將其當做 Collection 來用的話,那麼集合是空的。當然,這個類也是不允許傳遞 null 值的(併發包中的容器類好像都不支持插入 null 值,因爲 null 值往往用作其他用途,比如用於方法的返回值代表操作失敗)。
接下來,我們來看看具體的源碼實現吧,它的源碼不是很簡單的那種,我們需要先搞清楚它的設計思想。
源碼加註釋大概有 1200 行,我們先看大框架:
// 構造時,我們可以指定公平模式還是非公平模式,區別之後再說public SynchronousQueue(boolean fair) { transferer = fair ? new TransferQueue() : new TransferStack(); }abstract static class Transferer { // 從方法名上大概就知道,這個方法用於轉移元素,從生產者手上轉到消費者手上 // 也可以被動地,消費者調用這個方法來從生產者手上取元素 // 第一個參數 e 如果不是 null,代表場景爲:將元素從生產者轉移給消費者 // 如果是 null,代表消費者等待生產者提供元素,然後返回值就是相應的生產者提供的元素 // 第二個參數代表是否設置超時,如果設置超時,超時時間是第三個參數的值 // 返回值如果是 null,代表超時,或者中斷。具體是哪個,可以通過檢測中斷狀態得到。 abstract Object transfer(Object e, boolean timed, long nanos); }
Transferer 有兩個內部實現類,是因爲構造 SynchronousQueue 的時候,我們可以指定公平策略。公平模式意味着,所有的讀寫線程都遵守先來後到,FIFO 嘛,對應 TransferQueue。而非公平模式則對應 TransferStack。
SynchronousQueue採用隊列TransferQueue來實現公平性策略,採用堆棧TransferStack來實現非公平性策略,他們兩種都是通過鏈表實現的,其節點分別爲QNode,SNode。TransferQueue和TransferStack在SynchronousQueue中扮演着非常重要的作用,SynchronousQueue的put、take操作都是委託這兩個類來實現的。
我們先採用公平模式分析源碼,然後再說說公平模式和非公平模式的區別。
接下來,我們看看 put 方法和 take 方法:
// 寫入值public void put(E o) throws InterruptedException { if (o == null) throw new NullPointerException(); if (transferer.transfer(o, false, 0) == null) { // 1 Thread.interrupted(); throw new InterruptedException(); } }// 讀取值並移除public E take() throws InterruptedException { Object e = transferer.transfer(null, false, 0); // 2 if (e != null) return (E)e; Thread.interrupted(); throw new InterruptedException(); }
我們看到,寫操作 put(E o) 和讀操作 take() 都是調用 Transferer.transfer(…) 方法,區別在於第一個參數是否爲 null 值。
我們來看看 transfer 的設計思路,其基本算法如下:
當調用這個方法時,如果隊列是空的,或者隊列中的節點和當前的線程操作類型一致(如當前操作是 put 操作,而隊列中的元素也都是寫線程)。這種情況下,將當前線程加入到等待隊列即可。
如果隊列中有等待節點,而且與當前操作可以匹配(如隊列中都是讀操作線程,當前線程是寫操作線程,反之亦然)。這種情況下,匹配等待隊列的隊頭,出隊,返回相應數據。
其實這裏有個隱含的條件被滿足了,隊列如果不爲空,肯定都是同種類型的節點,要麼都是讀操作,要麼都是寫操作。這個就要看到底是讀線程積壓了,還是寫線程積壓了。
我們可以假設出一個男女配對的場景:一個男的過來,如果一個人都沒有,那麼他需要等待;如果發現有一堆男的在等待,那麼他需要排到隊列後面;如果發現是一堆女的在排隊,那麼他直接牽走隊頭的那個女的。
既然這裏說到了等待隊列,我們先看看其實現,也就是 QNode:
static final class QNode { volatile QNode next; // 可以看出來,等待隊列是單向鏈表 volatile Object item; // CAS'ed to or from null volatile Thread waiter; // 將線程對象保存在這裏,用於掛起和喚醒 final boolean isData; // 用於判斷是寫線程節點(isData == true),還是讀線程節點 QNode(Object item, boolean isData) { this.item = item; this.isData = isData; } ......
相信說了這麼多以後,我們再來看 transfer 方法的代碼就輕鬆多了。
/** * Puts or takes an item. */Object transfer(Object e, boolean timed, long nanos) { QNode s = null; // constructed/reused as needed boolean isData = (e != null); for (;;) { QNode t = tail; QNode h = head; if (t == null || h == null) // saw uninitialized value continue; // spin // 隊列空,或隊列中節點類型和當前節點一致, // 即我們說的第一種情況,將節點入隊即可。讀者要想着這塊 if 裏面方法其實就是入隊 if (h == t || t.isData == isData) { // empty or same-mode QNode tn = t.next; // t != tail 說明剛剛有節點入隊,continue 即可 if (t != tail) // inconsistent read continue; // 有其他節點入隊,但是 tail 還是指向原來的,此時設置 tail 即可 if (tn != null) { // lagging tail // 這個方法就是:如果 tail 此時爲 t 的話,設置爲 tn advanceTail(t, tn); continue; } // if (timed && nanos <= 0) // can't wait return null; if (s == null) s = new QNode(e, isData); // 將當前節點,插入到 tail 的後面 if (!t.casNext(null, s)) // failed to link in continue; // 將當前節點設置爲新的 tail advanceTail(t, s); // swing tail and wait // 看到這裏,請讀者先往下滑到這個方法,看完了以後再回來這裏,思路也就不會斷了 Object x = awaitFulfill(s, e, timed, nanos); // 到這裏,說明之前入隊的線程被喚醒了,準備往下執行 if (x == s) { // wait was cancelled clean(t, s); return null; } if (!s.isOffList()) { // not already unlinked advanceHead(t, s); // unlink if head if (x != null) // and forget fields s.item = s; s.waiter = null; } return (x != null) ? x : e; // 這裏的 else 分支就是上面說的第二種情況,有相應的讀或寫相匹配的情況 } else { // complementary-mode QNode m = h.next; // node to fulfill if (t != tail || m == null || h != head) continue; // inconsistent read Object x = m.item; if (isData == (x != null) || // m already fulfilled x == m || // m cancelled !m.casItem(x, e)) { // lost CAS advanceHead(h, m); // dequeue and retry continue; } advanceHead(h, m); // successfully fulfilled LockSupport.unpark(m.waiter); return (x != null) ? x : e; } } }void advanceTail(QNode t, QNode nt) { if (tail == t) UNSAFE.compareAndSwapObject(this, tailOffset, t, nt); }
// 自旋或阻塞,直到滿足條件,這個方法返回Object awaitFulfill(QNode s, Object e, boolean timed, long nanos) { long lastTime = timed ? System.nanoTime() : 0; Thread w = Thread.currentThread(); // 判斷需要自旋的次數, int spins = ((head.next == s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0); for (;;) { // 如果被中斷了,那麼取消這個節點 if (w.isInterrupted()) // 就是將當前節點 s 中的 item 屬性設置爲 this s.tryCancel(e); Object x = s.item; // 這裏是這個方法的唯一的出口 if (x != e) return x; // 如果需要,檢測是否超時 if (timed) { long now = System.nanoTime(); nanos -= now - lastTime; lastTime = now; if (nanos <= 0) { s.tryCancel(e); continue; } } if (spins > 0) --spins; // 如果自旋達到了最大的次數,那麼檢測 else if (s.waiter == null) s.waiter = w; // 如果自旋到了最大的次數,那麼線程掛起,等待喚醒 else if (!timed) LockSupport.park(this); // spinForTimeoutThreshold 這個之前講 AQS 的時候其實也說過,剩餘時間小於這個閾值的時候,就 // 不要進行掛起了,自旋的性能會比較好 else if (nanos > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanos); } }
Doug Lea 的巧妙之處在於,將各個代碼湊在了一起,使得代碼非常簡潔,當然也同時增加了我們的閱讀負擔,看代碼的時候,還是得仔細想想各種可能的情況。
下面,再說說前面說的公平模式和非公平模式的區別。
相信大家心裏面已經有了公平模式的工作流程的概念了,我就簡單說說 TransferStack 的算法,就不分析源碼了。
當調用這個方法時,如果隊列是空的,或者隊列中的節點和當前的線程操作類型一致(如當前操作是 put 操作,而棧中的元素也都是寫線程)。這種情況下,將當前線程加入到等待棧中,等待配對。然後返回相應的元素,或者如果被取消了的話,返回 null。
如果棧中有等待節點,而且與當前操作可以匹配(如棧裏面都是讀操作線程,當前線程是寫操作線程,反之亦然)。將當前節點壓入棧頂,和棧中的節點進行匹配,然後將這兩個節點出棧。配對和出棧的動作其實也不是必須的,因爲下面的一條會執行同樣的事情。
如果棧頂是進行匹配而入棧的節點,幫助其進行匹配並出棧,然後再繼續操作。
應該說,TransferStack 的源碼要比 TransferQueue 的複雜一些,如果讀者感興趣,請自行進行源碼閱讀。
BlockingQueue 實現之 PriorityBlockingQueue
帶排序的 BlockingQueue 實現,其併發控制採用的是 ReentrantLock,隊列爲×××隊列(ArrayBlockingQueue 是有界隊列,LinkedBlockingQueue 也可以通過在構造函數中傳入 capacity 指定隊列最大的容量,但是 PriorityBlockingQueue 只能指定初始的隊列大小,後面插入元素的時候,如果空間不夠的話會自動擴容)。
簡單地說,它就是 PriorityQueue 的線程安全版本。不可以插入 null 值,同時,插入隊列的對象必須是可比較大小的(comparable),否則報 ClassCastException 異常。它的插入操作 put 方法不會 block,因爲它是×××隊列(take 方法在隊列爲空的時候會阻塞)。
它的源碼相對比較簡單,本節將介紹其核心源碼部分。
我們來看看它有哪些屬性:
// 構造方法中,如果不指定大小的話,默認大小爲 11private static final int DEFAULT_INITIAL_CAPACITY = 11;// 數組的最大容量private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;// 這個就是存放數據的數組private transient Object[] queue;// 隊列當前大小private transient int size;// 大小比較器,如果按照自然序排序,那麼此屬性可設置爲 nullprivate transient Comparator<? super E> comparator;// 併發控制所用的鎖,所有的 public 且涉及到線程安全的方法,都必須先獲取到這個鎖private final ReentrantLock lock;// 這個很好理解,其實例由上面的 lock 屬性創建private final Condition notEmpty;// 這個也是用於鎖,用於數組擴容的時候,需要先獲取到這個鎖,才能進行擴容操作// 其使用 CAS 操作private transient volatile int allocationSpinLock;// 用於序列化和反序列化的時候用,對於 PriorityBlockingQueue 我們應該比較少使用到序列化private PriorityQueue q;
此類實現了 Collection 和 Iterator 接口中的所有接口方法,對其對象進行迭代並遍歷時,不能保證有序性。如果你想要實現有序遍歷,建議採用 Arrays.sort(queue.toArray()) 進行處理。PriorityBlockingQueue 提供了 drainTo 方法用於將部分或全部元素有序地填充(準確說是轉移,會刪除原隊列中的元素)到另一個集合中。還有一個需要說明的是,如果兩個對象的優先級相同(compare 方法返回 0),此隊列並不保證它們之間的順序。
PriorityBlockingQueue 使用了基於數組的二叉堆來存放元素,所有的 public 方法採用同一個 lock 進行併發控制。
二叉堆:一顆完全二叉樹,它非常適合用數組進行存儲,對於數組中的元素 a[i],其左子節點爲 a[2i+1],其右子節點爲 a[2\i + 2],其父節點爲 a[(i-1)/2],其堆序性質爲,每個節點的值都小於其左右子節點的值。二叉堆中最小的值就是根節點,但是刪除根節點是比較麻煩的,因爲需要調整樹。
簡單用個圖解釋一下二叉堆,我就不說太多專業的嚴謹的術語了,這種數據結構的優點是一目瞭然的,最小的元素一定是根元素,它是一棵滿的樹,除了最後一層,最後一層的節點從左到右緊密排列。
下面開始 PriorityBlockingQueue 的源碼分析,首先我們來看看構造方法:
// 默認構造方法,採用默認值(11)來進行初始化public PriorityBlockingQueue() { this(DEFAULT_INITIAL_CAPACITY, null); }// 指定數組的初始大小public PriorityBlockingQueue(int initialCapacity) { this(initialCapacity, null); }// 指定比較器public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.lock = new ReentrantLock(); this.notEmpty = lock.newCondition(); this.comparator = comparator; this.queue = new Object[initialCapacity]; }// 在構造方法中就先填充指定的集合中的元素public PriorityBlockingQueue(Collection<? extends E> c) { this.lock = new ReentrantLock(); this.notEmpty = lock.newCondition(); // boolean heapify = true; // true if not known to be in heap order boolean screen = true; // true if must screen for nulls if (c instanceof SortedSet<?>) { SortedSet<? extends E> ss = (SortedSet<? extends E>) c; this.comparator = (Comparator<? super E>) ss.comparator(); heapify = false; } else if (c instanceof PriorityBlockingQueue<?>) { PriorityBlockingQueue<? extends E> pq = (PriorityBlockingQueue<? extends E>) c; this.comparator = (Comparator<? super E>) pq.comparator(); screen = false; if (pq.getClass() == PriorityBlockingQueue.class) // exact match heapify = false; } Object[] a = c.toArray(); int n = a.length; // If c.toArray incorrectly doesn't return Object[], copy it. if (a.getClass() != Object[].class) a = Arrays.copyOf(a, n, Object[].class); if (screen && (n == 1 || this.comparator != null)) { for (int i = 0; i < n; ++i) if (a[i] == null) throw new NullPointerException(); } this.queue = a; this.size = n; if (heapify) heapify(); }
接下來,我們來看看其內部的自動擴容實現:
private void tryGrow(Object[] array, int oldCap) { // 這邊做了釋放鎖的操作 lock.unlock(); // must release and then re-acquire main lock Object[] newArray = null; // 用 CAS 操作將 allocationSpinLock 由 0 變爲 1,也算是獲取鎖 if (allocationSpinLock == 0 && UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) { try { // 如果節點個數小於 64,那麼增加的 oldCap + 2 的容量 // 如果節點數大於等於 64,那麼增加 oldCap 的一半 // 所以節點數較小時,增長得快一些 int newCap = oldCap + ((oldCap < 64) ? (oldCap + 2) : (oldCap >> 1)); // 這裏有可能溢出 if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow int minCap = oldCap + 1; if (minCap < 0 || minCap > MAX_ARRAY_SIZE) throw new OutOfMemoryError(); newCap = MAX_ARRAY_SIZE; } // 如果 queue != array,那麼說明有其他線程給 queue 分配了其他的空間 if (newCap > oldCap && queue == array) // 分配一個新的大數組 newArray = new Object[newCap]; } finally { // 重置,也就是釋放鎖 allocationSpinLock = 0; } } // 如果有其他的線程也在做擴容的操作 if (newArray == null) // back off if another thread is allocating Thread.yield(); // 重新獲取鎖 lock.lock(); // 將原來數組中的元素複製到新分配的大數組中 if (newArray != null && queue == array) { queue = newArray; System.arraycopy(array, 0, newArray, 0, oldCap); } }
擴容方法對併發的控制也非常的巧妙,釋放了原來的獨佔鎖 lock,這樣的話,擴容操作和讀操作可以同時進行,提高吞吐量。
下面,我們來分析下寫操作 put 方法和讀操作 take 方法。
public void put(E e) { // 直接調用 offer 方法,因爲前面我們也說了,在這裏,put 方法不會阻塞 offer(e); }public boolean offer(E e) { if (e == null) throw new NullPointerException(); final ReentrantLock lock = this.lock; // 首先獲取到獨佔鎖 lock.lock(); int n, cap; Object[] array; // 如果當前隊列中的元素個數 >= 數組的大小,那麼需要擴容了 while ((n = size) >= (cap = (array = queue).length)) tryGrow(array, cap); try { Comparator<? super E> cmp = comparator; // 節點添加到二叉堆中 if (cmp == null) siftUpComparable(n, e, array); else siftUpUsingComparator(n, e, array, cmp); // 更新 size size = n + 1; // 喚醒等待的讀線程 notEmpty.signal(); } finally { lock.unlock(); } return true; }
對於二叉堆而言,插入一個節點是簡單的,插入的節點如果比父節點小,交換它們,然後繼續和父節點比較。
// 這個方法就是將數據 x 插入到數組 array 的位置 k 處,然後再調整樹private static <T> void siftUpComparable(int k, T x, Object[] array) { Comparable<? super T> key = (Comparable<? super T>) x; while (k > 0) { // 二叉堆中 a[k] 節點的父節點位置 int parent = (k - 1) >>> 1; Object e = array[parent]; if (key.compareTo((T) e) >= 0) break; array[k] = e; k = parent; } array[k] = key; }
我們用圖來示意一下,我們接下來要將 11 插入到隊列中,看看 siftUp 是怎麼操作的。
我們再看看 take 方法:
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; // 獨佔鎖 lock.lockInterruptibly(); E result; try { // dequeue 出隊 while ( (result = dequeue()) == null) notEmpty.await(); } finally { lock.unlock(); } return result; }
private E dequeue() { int n = size - 1; if (n < 0) return null; else { Object[] array = queue; // 隊頭,用於返回 E result = (E) array[0]; // 隊尾元素先取出 E x = (E) array[n]; // 隊尾置空 array[n] = null; Comparator<? super E> cmp = comparator; if (cmp == null) siftDownComparable(0, x, array, n); else siftDownUsingComparator(0, x, array, n, cmp); size = n; return result; } }
dequeue 方法返回隊頭,並調整二叉堆的樹,調用這個方法必須先獲取獨佔鎖。
廢話不多說,出隊是非常簡單的,因爲隊頭就是最小的元素,對應的是數組的第一個元素。難點是隊頭出隊後,需要調整樹。
private static <T> void siftDownComparable(int k, T x, Object[] array, int n) { if (n > 0) { Comparable<? super T> key = (Comparable<? super T>)x; // 這裏得到的 half 肯定是非葉節點 // a[n] 是最後一個元素,其父節點是 a[(n-1)/2]。所以 n >>> 1 代表的節點肯定不是葉子節點 // 下面,我們結合圖來一行行分析,這樣比較直觀簡單 // 此時 k 爲 0, x 爲 17,n 爲 9 int half = n >>> 1; // 得到 half = 4 while (k < half) { // 先取左子節點 int child = (k << 1) + 1; // 得到 child = 1 Object c = array[child]; // c = 12 int right = child + 1; // right = 2 // 如果右子節點存在,而且比左子節點小 // 此時 array[right] = 20,所以條件不滿足 if (right < n && ((Comparable<? super T>) c).compareTo((T) array[right]) > 0) c = array[child = right]; // key = 17, c = 12,所以條件不滿足 if (key.compareTo((T) c) <= 0) break; // 把 12 填充到根節點 array[k] = c; // k 賦值後爲 1 k = child; // 一輪過後,我們發現,12 左邊的子樹和剛剛的差不多,都是缺少根節點,接下來處理就簡單了 } array[k] = key; } }
記住二叉堆是一棵完全二叉樹,那麼根節點 10 拿掉後,最後面的元素 17 必須找到合適的地方放置。首先,17 和 10 不能直接交換,那麼先將根節點 10 的左右子節點中較小的節點往上滑,即 12 往上滑,然後原來 12 留下了一個空節點,然後再把這個空節點的較小的子節點往上滑,即 13 往上滑,最後,留出了位子,17 補上即可。
我稍微調整下這個樹,以便讀者能更明白:
好了, PriorityBlockingQueue 我們也說完了。
DelayQueue
原文出處http://cmsblogs.com/ 『chenssy』
DelayQueue是一個支持延時獲取元素的×××阻塞隊列。裏面的元素全部都是“可延期”的元素,列頭的元素是最先“到期”的元素,如果隊列裏面沒有元素到期,是不能從列頭獲取元素的,哪怕有元素也不行。也就是說只有在延遲期到時才能夠從隊列中取元素。
DelayQueue主要用於兩個方面:
- 緩存:清掉緩存中超時的緩存數據
- 任務超時處理
DelayQueue
DelayQueue實現的關鍵主要有如下幾個:
可重入鎖ReentrantLock
用於阻塞和通知的Condition對象
根據Delay時間排序的優先級隊列:PriorityQueue
用於優化阻塞通知的線程元素leader
ReentrantLock、Condition這兩個對象就不需要闡述了,他是實現整個BlockingQueue的核心。PriorityQueue是一個支持優先級線程排序的隊列(參考【死磕Java併發】-----J.U.C之阻塞隊列:PriorityBlockingQueue),leader後面闡述。這裏我們先來了解Delay,他是實現延時操作的關鍵。
Delayed
Delayed接口是用來標記那些應該在給定延遲時間之後執行的對象,它定義了一個long getDelay(TimeUnit unit)方法,該方法返回與此對象相關的的剩餘時間。同時實現該接口的對象必須定義一個compareTo 方法,該方法提供與此接口的 getDelay 方法一致的排序。
public interface Delayed extends Comparable<Delayed> { long getDelay(TimeUnit unit);}
如何使用該接口呢?上面說的非常清楚了,實現該接口的getDelay()方法,同時定義compareTo()方法即可。
內部結構
先看DelayQueue的定義:
public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> { /** 可重入鎖 */ private final transient ReentrantLock lock = new ReentrantLock(); /** 支持優先級的BlockingQueue */ private final PriorityQueue<E> q = new PriorityQueue<E>(); /** 用於優化阻塞 */ private Thread leader = null; /** Condition */ private final Condition available = lock.newCondition(); /** * 省略很多代碼 */ }
看了DelayQueue的內部結構就對上面幾個關鍵點一目瞭然了,但是這裏有一點需要注意,DelayQueue的元素都必須繼承Delayed接口。同時也可以從這裏初步理清楚DelayQueue內部實現的機制了:以支持優先級×××隊列的PriorityQueue作爲一個容器,容器裏面的元素都應該實現Delayed接口,在每次往優先級隊列中添加元素時以元素的過期時間作爲排序條件,最先過期的元素放在優先級最高。
offer()
public boolean offer(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { // 向 PriorityQueue中插入元素 q.offer(e); // 如果當前元素的對首元素(優先級最高),leader設置爲空,喚醒所有等待線程 if (q.peek() == e) { leader = null; available.signal(); } // ×××隊列,永遠返回true return true; } finally { lock.unlock(); } }
offer(E e)就是往PriorityQueue中添加元素,具體可以參考(【死磕Java併發】-----J.U.C之阻塞隊列:PriorityBlockingQueue)。整個過程還是比較簡單,但是在判斷當前元素是否爲對首元素,如果是的話則設置leader=null,這是非常關鍵的一個步驟,後面闡述。
take()
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (;;) { // 對首元素 E first = q.peek(); // 對首爲空,阻塞,等待off()操作喚醒 if (first == null) available.await(); else { // 獲取對首元素的超時時間 long delay = first.getDelay(NANOSECONDS); // <=0 表示已過期,出對,return if (delay <= 0) return q.poll(); first = null; // don't retain ref while waiting // leader != null 證明有其他線程在操作,阻塞 if (leader != null) available.await(); else { // 否則將leader 設置爲當前線程,獨佔 Thread thisThread = Thread.currentThread(); leader = thisThread; try { // 超時阻塞 available.awaitNanos(delay); } finally { // 釋放leader if (leader == thisThread) leader = null; } } } } } finally { // 喚醒阻塞線程 if (leader == null && q.peek() != null) available.signal(); lock.unlock(); } }
首先是獲取對首元素,如果對首元素的延時時間 delay <= 0 ,則可以出對了,直接return即可。否則設置first = null,這裏設置爲null的主要目的是爲了避免內存泄漏。如果 leader != null 則表示當前有線程佔用,則阻塞,否則設置leader爲當前線程,然後調用awaitNanos()方法超時等待。
first = null
這裏爲什麼如果不設置first = null,則會引起內存泄漏呢?線程A到達,列首元素沒有到期,設置leader = 線程A,這是線程B來了因爲leader != null,則會阻塞,線程C一樣。假如線程阻塞完畢了,獲取列首元素成功,出列。這個時候列首元素應該會被回收掉,但是問題是它還被線程B、線程C持有着,所以不會回收,這裏只有兩個線程,如果有線程D、線程E...呢?這樣會無限期的不能回收,就會造成內存泄漏。
這個入隊、出對過程和其他的阻塞隊列沒有很大區別,無非是在出對的時候增加了一個到期時間的判斷。同時通過leader來減少不必要阻塞。
ConcurrentLinkedQueue
原文出處http://cmsblogs.com/ 『chenssy』
要實現一個線程安全的隊列有兩種方式:阻塞和非阻塞。阻塞隊列無非就是鎖的應用,而非阻塞則是CAS算法的應用。下面我們就開始一個非阻塞算法的研究:CoucurrentLinkedQueue。
ConcurrentLinkedQueue是一個基於鏈接節點的無邊界的線程安全隊列,它採用FIFO原則對元素進行排序。採用“wait-free”算法(即CAS算法)來實現的。
CoucurrentLinkedQueue規定了如下幾個不變性:
在入隊的最後一個元素的next爲null
隊列中所有未刪除的節點的item都不能爲null且都能從head節點遍歷到
對於要刪除的節點,不是直接將其設置爲null,而是先將其item域設置爲null(迭代器會跳過item爲null的節點)
允許head和tail更新滯後。這是什麼意思呢?意思就說是head、tail不總是指向第一個元素和最後一個元素(後面闡述)。
head的不變性和可變性:
不變性
所有未刪除的節點都可以通過head節點遍歷到
head不能爲null
head節點的next不能指向自身
可變性
head的item可能爲null,也可能不爲null
2.允許tail滯後head,也就是說調用succc()方法,從head不可達tail
tail的不變性和可變性
不變性
tail不能爲null
可變性
tail的item可能爲null,也可能不爲null
tail節點的next域可以指向自身
3.允許tail滯後head,也就是說調用succc()方法,從head不可達tail
這些特性是否已經暈了?沒關係,我們看下面的源碼分析就可以理解這些特性了。
ConcurrentLinkedQueue源碼分析
CoucurrentLinkedQueue的結構由head節點和tail節點組成,每個節點由節點元素item和指向下一個節點的next引用組成,而節點與節點之間的關係就是通過該next關聯起來的,從而組成一張鏈表的隊列。節點Node爲ConcurrentLinkedQueue的內部類,定義如下:
private static class Node<E> { /** 節點元素域 */ volatile E item; volatile Node<E> next; //初始化,獲得item 和 next 的偏移量,爲後期的CAS做準備 Node(E item) { UNSAFE.putObject(this, itemOffset, item); } boolean casItem(E cmp, E val) { return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); } void lazySetNext(Node<E> val) { UNSAFE.putOrderedObject(this, nextOffset, val); } boolean casNext(Node<E> cmp, Node<E> val) { return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); } // Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; /** 偏移量 */ private static final long itemOffset; /** 下一個元素的偏移量 */ private static final long nextOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> k = Node.class; itemOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("item")); nextOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("next")); } catch (Exception e) { throw new Error(e); } } }private static class Node<E> { /** 節點元素域 */ volatile E item; volatile Node<E> next; //初始化,獲得item 和 next 的偏移量,爲後期的CAS做準備 Node(E item) { UNSAFE.putObject(this, itemOffset, item); } boolean casItem(E cmp, E val) { return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); } void lazySetNext(Node<E> val) { UNSAFE.putOrderedObject(this, nextOffset, val); } boolean casNext(Node<E> cmp, Node<E> val) { return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); } // Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; /** 偏移量 */ private static final long itemOffset; /** 下一個元素的偏移量 */ private static final long nextOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> k = Node.class; itemOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("item")); nextOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("next")); } catch (Exception e) { throw new Error(e); } } }
入列
入列,我們認爲是一個非常簡單的過程:tail節點的next執行新節點,然後更新tail爲新節點即可。從單線程角度我們這麼理解應該是沒有問題的,但是多線程呢?如果一個線程正在進行插入動作,那麼它必須先獲取尾節點,然後設置尾節點的下一個節點爲當前節點,但是如果已經有一個線程剛剛好完成了插入,那麼尾節點是不是發生了變化?對於這種情況ConcurrentLinkedQueue怎麼處理呢?我們先看源碼:
offer(E e):將指定元素插入都隊列尾部:
public boolean offer(E e) { //檢查節點是否爲null checkNotNull(e); // 創建新節點 final Node<E> newNode = new Node<E>(e); //死循環 直到成功爲止 for (Node<E> t = tail, p = t;;) { Node<E> q = p.next; // q == null 表示 p已經是最後一個節點了,嘗試加入到隊列尾 // 如果插入失敗,則表示其他線程已經修改了p的指向 if (q == null) { // --- 1 // casNext:t節點的next指向當前節點 // casTail:設置tail 尾節點 if (p.casNext(null, newNode)) { // --- 2 // node 加入節點後會導致tail距離最後一個節點相差大於一個,需要更新tail if (p != t) // --- 3 casTail(t, newNode); // --- 4 return true; } } // p == q 等於自身 else if (p == q) // --- 5 // p == q 代表着該節點已經被刪除了 // 由於多線程的原因,我們offer()的時候也會poll(),如果offer()的時候正好該節點已經poll()了 // 那麼在poll()方法中的updateHead()方法會將head指向當前的q,而把p.next指向自己,即:p.next == p // 這樣就會導致tail節點滯後head(tail位於head的前面),則需要重新設置p p = (t != (t = tail)) ? t : head; // --- 6 // tail並沒有指向尾節點 else // tail已經不是最後一個節點,將p指向最後一個節點 p = (p != t && t != (t = tail)) ? t : q; // --- 7 } } boolean offer(E e) { //檢查節點是否爲null checkNotNull(e); // 創建新節點 final Node<E> newNode = new Node<E>(e); //死循環 直到成功爲止 for (Node<E> t = tail, p = t;;) { Node<E> q = p.next; // q == null 表示 p已經是最後一個節點了,嘗試加入到隊列尾 // 如果插入失敗,則表示其他線程已經修改了p的指向 if (q == null) { // --- 1 // casNext:t節點的next指向當前節點 // casTail:設置tail 尾節點 if (p.casNext(null, newNode)) { // --- 2 // node 加入節點後會導致tail距離最後一個節點相差大於一個,需要更新tail if (p != t) // --- 3 casTail(t, newNode); // --- 4 return true; } } // p == q 等於自身 else if (p == q) // --- 5 // p == q 代表着該節點已經被刪除了 // 由於多線程的原因,我們offer()的時候也會poll(),如果offer()的時候正好該節點已經poll()了 // 那麼在poll()方法中的updateHead()方法會將head指向當前的q,而把p.next指向自己,即:p.next == p // 這樣就會導致tail節點滯後head(tail位於head的前面),則需要重新設置p p = (t != (t = tail)) ? t : head; // --- 6 // tail並沒有指向尾節點 else // tail已經不是最後一個節點,將p指向最後一個節點 p = (p != t && t != (t = tail)) ? t : q; // --- 7 } }
光看源碼還是有點兒迷糊的,插入節點一次分析就會明朗很多。
初始化
ConcurrentLinkedQueue初始化時head、tail存儲的元素都爲null,且head等於tail:
添加元素A
按照程序分析:第一次插入元素A,head = tail = dummyNode,所有q = p.next = null,直接走步驟2:p.casNext(null, newNode),由於 p == t成立,所以不會執行步驟3:casTail(t, newNode),直接return。插入A節點後如下:
添加元素B
q = p.next = A ,p = tail = dummyNode,所以直接跳到步驟7:p = (p != t && t != (t = tail)) ? t : q;。此時p = q,然後進行第二次循環 q = p.next = null,步驟2:p == null成立,將該節點插入,因爲p = q,t = tail,所以步驟3:p != t 成立,執行步驟4:casTail(t, newNode),然後return。如下:
轉存失敗重新上傳取消
添加節點C
此時t = tail ,p = t,q = p.next = null,和插入元素A無異,如下:
轉存失敗重新上傳取消
這裏整個offer()過程已經分析完成了,可能p == q 有點兒難理解,p 不是等於q.next麼,怎麼會有p == q呢?這個疑問我們在出列poll()中分析
出列
ConcurrentLinkedQueue提供了poll()方法進行出列操作。入列主要是涉及到tail,出列則涉及到head。我們先看源碼:
public E poll() { // 如果出現p被刪除的情況需要從head重新開始 restartFromHead: // 這是什麼語法?真心沒有見過 for (;;) { for (Node<E> h = head, p = h, q;;) { // 節點 item E item = p.item; // item 不爲null,則將item 設置爲null if (item != null && p.casItem(item, null)) { // --- 1 // p != head 則更新head if (p != h) // --- 2 // p.next != null,則將head更新爲p.next ,否則更新爲p updateHead(h, ((q = p.next) != null) ? q : p); // --- 3 return item; } // p.next == null 隊列爲空 else if ((q = p.next) == null) { // --- 4 updateHead(h, p); return null; } // 當一個線程在poll的時候,另一個線程已經把當前的p從隊列中刪除——將p.next = p,p已經被移除不能繼續,需要重新開始 else if (p == q) // --- 5 continue restartFromHead; else p = q; // --- 6 } } } E poll() { // 如果出現p被刪除的情況需要從head重新開始 restartFromHead: // 這是什麼語法?真心沒有見過 for (;;) { for (Node<E> h = head, p = h, q;;) { // 節點 item E item = p.item; // item 不爲null,則將item 設置爲null if (item != null && p.casItem(item, null)) { // --- 1 // p != head 則更新head if (p != h) // --- 2 // p.next != null,則將head更新爲p.next ,否則更新爲p updateHead(h, ((q = p.next) != null) ? q : p); // --- 3 return item; } // p.next == null 隊列爲空 else if ((q = p.next) == null) { // --- 4 updateHead(h, p); return null; } // 當一個線程在poll的時候,另一個線程已經把當前的p從隊列中刪除——將p.next = p,p已經被移除不能繼續,需要重新開始 else if (p == q) // --- 5 continue restartFromHead; else p = q; // --- 6 } } }
這個相對於offer()方法而言會簡單些,裏面有一個很重要的方法:updateHead(),該方法用於CAS更新head節點,如下:
final void updateHead(Node<E> h, Node<E> p) { if (h != p && casHead(h, p)) h.lazySetNext(h); }final void updateHead(Node<E> h, Node<E> p) { if (h != p && casHead(h, p)) h.lazySetNext(h); }
我們先將上面offer()的鏈表poll()掉,添加A、B、C節點結構如下:
poll A
head = dumy,p = head, item = p.item = null,步驟1不成立,步驟4:(q = p.next) == null不成立,p.next = A,跳到步驟6,下一個循環,此時p = A,所以步驟1 item != null,進行p.casItem(item, null)成功,此時p == A != h,所以執行步驟3:updateHead(h, ((q = p.next) != null) ? q : p),q = p.next = B != null,則將head CAS更新成B,如下:
poll B
head = B , p = head = B,item = p.item = B,步驟成立,步驟2:p != h 不成立,直接return,如下:
poll C
head = dumy ,p = head = dumy,tiem = p.item = null,步驟1不成立,跳到步驟4:(q = p.next) == null,不成立,然後跳到步驟6,此時,p = q = C,item = C(item),步驟1成立,所以講C(item)設置爲null,步驟2:p != h成立,執行步驟3:updateHead(h, ((q = p.next) != null) ? q : p),如下:
看到這裏是不是一目瞭然了,在這裏我們再來分析offer()的步驟5:
else if(p == q){ p = (t != (t = tail))? t : head;} if(p == q){ p = (t != (t = tail))? t : head;}
ConcurrentLinkedQueue中規定,p == q表明,該節點已經被刪除了,也就說tail滯後於head,head無法通過succ()方法遍歷到tail,怎麼做? (t != (t = tail))? t : head;(這段代碼的可讀性實在是太差了,真他媽難理解:不知道是否可以理解爲t != tail ? tail : head)這段代碼主要是來判讀tail節點是否已經發生了改變,如果發生了改變,則說明tail已經重新定位了,只需要重新找到tail即可,否則就只能指向head了。
就上面那個我們再次插入一個元素D。則p = head,q = p.next = null,執行步驟1: q = null且 p != t ,所以執行步驟4:,如下:
再插入元素E,q = p.next = null,p == t,所以插入E後如下:
到這裏ConcurrentLinkedQueue的整個入列、出列都已經分析完畢了,對於ConcurrentLinkedQueue LZ真心感覺難看懂,看懂之後也感嘆設計得太精妙了,利用CAS來完成數據操作,同時允許隊列的不一致性,這種弱一致性確實是非常強大。再次感嘆Doug Lea的天才。
LinkedTransferQueue
原文出處http://cmsblogs.com/ 『chenssy』
前面提到的各種BlockingQueue對讀或者寫都是鎖上整個隊列,在併發量大的時候,各種鎖是比較耗資源和耗時間的,而前面的SynchronousQueue雖然不會鎖住整個隊列,但它是一個沒有容量的“隊列”,那麼有沒有這樣一種隊列,它即可以像其他的BlockingQueue一樣有容量又可以像SynchronousQueue一樣不會鎖住整個隊列呢?有!答案就是LinkedTransferQueue。
LinkedTransferQueue是基於鏈表的FIFO×××阻塞隊列,它出現在JDK7中。Doug Lea 大神說LinkedTransferQueue是一個聰明的隊列。它是ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、×××的LinkedBlockingQueues等的超集。既然這麼牛逼,那勢必要弄清楚其中的原理了。
LinkedTransferQueue
看源碼之前我們先稍微瞭解下它的原理,這樣看源碼就會有跡可循了。
LinkedTransferQueue採用一種預佔模式。什麼意思呢?有就直接拿走,沒有就佔着這個位置直到拿到或者超時或者中斷。即消費者線程到隊列中取元素時,如果發現隊列爲空,則會生成一個null節點,然後park住等待生產者。後面如果生產者線程入隊時發現有一個null元素節點,這時生產者就不會入列了,直接將元素填充到該節點上,喚醒該節點的線程,被喚醒的消費者線程拿東西走人。是不是有點兒SynchronousQueue的味道?
結構
LinkedTransferQueue與其他的BlockingQueue一樣,同樣繼承AbstractQueue類,但是它實現了TransferQueue,TransferQueue接口繼承BlockingQueue,所以TransferQueue算是對BlockingQueue一種擴充,該接口提供了一整套的transfer接口:
public interface TransferQueue<E> extends BlockingQueue<E> { /** * 若當前存在一個正在等待獲取的消費者線程(使用take()或者poll()函數),使用該方法會即刻轉移/傳輸對象元素e; * 若不存在,則返回false,並且不進入隊列。這是一個不阻塞的操作 */ boolean tryTransfer(E e); /** * 若當前存在一個正在等待獲取的消費者線程,即立刻移交之; * 否則,會插入當前元素e到隊列尾部,並且等待進入阻塞狀態,到有消費者線程取走該元素 */ void transfer(E e) throws InterruptedException; /** * 若當前存在一個正在等待獲取的消費者線程,會立即傳輸給它;否則將插入元素e到隊列尾部,並且等待被消費者線程獲取消費掉; * 若在指定的時間內元素e無法被消費者線程獲取,則返回false,同時該元素被移除。 */ boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException; /** * 判斷是否存在消費者線程 */ boolean hasWaitingConsumer(); /** * 獲取所有等待獲取元素的消費線程數量 */ int getWaitingConsumerCount(); }
相對於其他的BlockingQueue,LinkedTransferQueue就多了上面幾個方法。這幾個方法在LinkedTransferQueue中起到了核心作用。
LinkedTransferQueue定義的變量如下:
// 判斷是否爲多核 private static final boolean MP = Runtime.getRuntime().availableProcessors() > 1; // 自旋次數 private static final int FRONT_SPINS = 1 << 7; // 前驅節點正在處理,當前節點需要自旋的次數 private static final int CHAINED_SPINS = FRONT_SPINS >>> 1; static final int SWEEP_THRESHOLD = 32; // 頭節點 transient volatile Node head; // 尾節點 private transient volatile Node tail; // 刪除節點失敗的次數 private transient volatile int sweepVotes; /* * 調用xfer()方法時需要傳入,區分不同處理 * xfer()方法是LinkedTransferQueue的最核心的方法 */ private static final int NOW = 0; // for untimed poll, tryTransfer private static final int ASYNC = 1; // for offer, put, add private static final int SYNC = 2; // for transfer, take private static final int TIMED = 3; // for timed poll, tryTransfer
Node節點
Node節點由四個部分構成:
isData:表示該節點是存放數據還是獲取數據
item:存放數據,isData爲false時,該節點爲null,爲true時,匹配後,該節點會置爲null
next:指向下一個節點
waiter:park住消費者線程,線程就放在這裏
結構如下:
源碼如下:
static final class Node { // 表示該節點是存放數據還是獲取數據 final boolean isData; // 存放數據,isData爲false時,該節點爲null,爲true時,匹配後,該節點會置爲null volatile Object item; //指向下一個節點 volatile Node next; // park住消費者線程,線程就放在這裏 volatile Thread waiter; // null until waiting /** * CAS Next域 */ final boolean casNext(Node cmp, Node val) { return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); } /** * CAS itme域 */ final boolean casItem(Object cmp, Object val) { return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); } /** * 構造函數 */ Node(Object item, boolean isData) { UNSAFE.putObject(this, itemOffset, item); // relaxed write this.isData = isData; } /** * 將next域指向自身,其實就是剔除節點 */ final void forgetNext() { UNSAFE.putObject(this, nextOffset, this); } /** * 匹配過或節點被取消的時候會調用 */ final void forgetContents() { UNSAFE.putObject(this, itemOffset, this); UNSAFE.putObject(this, waiterOffset, null); } /** * 校驗節點是否匹配過,如果匹配做取消了,item則會發生變化 */ final boolean isMatched() { Object x = item; return (x == this) || ((x == null) == isData); } /** * 是否是一個未匹配的請求節點 * 如果是的話isData應爲false,item == null,因位如果匹配了,item則會有值 */ final boolean isUnmatchedRequest() { return !isData && item == null; } /** * 如給定節點類型不能掛在當前節點後返回true */ final boolean cannotPrecede(boolean haveData) { boolean d = isData; Object x; return d != haveData && (x = item) != this && (x != null) == d; } /** * 匹配一個數據節點 */ final boolean tryMatchData() { // assert isData; Object x = item; if (x != null && x != this && casItem(x, null)) { LockSupport.unpark(waiter); return true; } return false; } private static final long serialVersionUID = -3375979862319811754L; // Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; private static final long itemOffset; private static final long nextOffset; private static final long waiterOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> k = Node.class; itemOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("item")); nextOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("next")); waiterOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("waiter")); } catch (Exception e) { throw new Error(e); } } }
節點Node爲LinkedTransferQueue的內部類,其內部結構和公平方式的SynchronousQueue差不多,裏面也同樣提供了一些很重要的方法。
put操作
LinkedTransferQueue提供了add、put、offer三類方法,用於將元素插入隊列中,如下:
public void put(E e) { xfer(e, true, ASYNC, 0); } public boolean offer(E e, long timeout, TimeUnit unit) { xfer(e, true, ASYNC, 0); return true; } public boolean offer(E e) { xfer(e, true, ASYNC, 0); return true; } public boolean add(E e) { xfer(e, true, ASYNC, 0); return true; }
由於LinkedTransferQueue是×××的,不會阻塞,所以在調用xfer方法是傳入的是ASYNC,同時直接返回true.
take操作
LinkedTransferQueue提供了poll、take方法用於出列元素:
public E take() throws InterruptedException { E e = xfer(null, false, SYNC, 0); if (e != null) return e; Thread.interrupted(); throw new InterruptedException(); } public E poll() { return xfer(null, false, NOW, 0); } public E poll(long timeout, TimeUnit unit) throws InterruptedException { E e = xfer(null, false, TIMED, unit.toNanos(timeout)); if (e != null || !Thread.interrupted()) return e; throw new InterruptedException(); }
這裏和put操作有點不一樣,take()方法傳入的是SYNC,阻塞。poll()傳入的是NOW,poll(long timeout, TimeUnit unit)則是傳入TIMED。
tranfer操作
實現TransferQueue接口,就要實現它的方法:
public boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException { if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null) return true; if (!Thread.interrupted()) return false; throw new InterruptedException();}public void transfer(E e) throws InterruptedException { if (xfer(e, true, SYNC, 0) != null) { Thread.interrupted(); // failure possible only due to interrupt throw new InterruptedException(); }}public boolean tryTransfer(E e) { return xfer(e, true, NOW, 0) == null;}
xfer()
通過上面幾個核心方法的源碼我們清楚可以看到,最終都是調用xfer()方法,該方法接受四個參數,item或者null的E,put操作爲true、take操作爲false的havaData,how(有四個值NOW, ASYNC, SYNC, or TIMED,分別表示不同的操作),超時nanos。
private E xfer(E e, boolean haveData, int how, long nanos) { // havaData爲true,但是e == null 拋出空指針 if (haveData && (e == null)) throw new NullPointerException(); Node s = null; // the node to append, if needed retry: for (;;) { // 從首節點開始匹配 // p == null 隊列爲空 for (Node h = head, p = h; p != null;) { // 模型,request or data boolean isData = p.isData; // item域 Object item = p.item; // 找到一個沒有匹配的節點 // item != p 也就是自身,則表示沒有匹配過 // (item != null) == isData,表示模型符合 if (item != p && (item != null) == isData) { // 節點類型和待處理類型一致,這樣肯定是不能匹配的 if (isData == haveData) // can't match break; // 匹配,將E加入到item域中 // 如果p 的item爲data,那麼e爲null,如果p的item爲null,那麼e爲data if (p.casItem(item, e)) { // match // for (Node q = p; q != h;) { Node n = q.next; // update by 2 unless singleton if (head == h && casHead(h, n == null ? q : n)) { h.forgetNext(); break; } // advance and retry if ((h = head) == null || (q = h.next) == null || !q.isMatched()) break; // unless slack < 2 } // 匹配後喚醒p的waiter線程;reservation則叫人收貨,data則叫null收貨 LockSupport.unpark(p.waiter); return LinkedTransferQueue.<E>cast(item); } } // 如果已經匹配了則向前推進 Node n = p.next; // 如果p的next指向p本身,說明p節點已經有其他線程處理過了,只能從head重新開始 p = (p != n) ? n : (h = head); // Use head if p offlist } // 如果沒有找到匹配的節點,則進行處理 // NOW爲untimed poll, tryTransfer,不需要入隊 if (how != NOW) { // No matches available // s == null,新建一個節點 if (s == null) s = new Node(e, haveData); // 入隊,返回前驅節點 Node pred = tryAppend(s, haveData); // 返回的前驅節點爲null,那就是有race,被其他的搶了,那就continue 整個for if (pred == null) continue retry; // ASYNC不需要阻塞等待 if (how != ASYNC) return awaitMatch(s, pred, e, (how == TIMED), nanos); } return e; } }
整個算法的核心就是尋找匹配節點找到了就返回,否則就入隊(NOW直接返回):
matched。判斷匹配條件(isData不一樣,本身沒有匹配),匹配後就casItem,然後unpark匹配節點的waiter線程,如果是reservation則叫人收貨,data則叫null收貨。
unmatched。如果沒有找到匹配節點,則根據傳入的how來處理,NOW直接返回,其餘三種先入對,入隊後如果是ASYNC則返回,SYNC和TIMED則會阻塞等待匹配。
其實相當於SynchronousQueue來說,這個處理邏輯還是比較簡單的。
如果沒有找到匹配節點,且how != NOW會入隊,入隊則是調用tryAppend方法:
private Node tryAppend(Node s, boolean haveData) { // 從尾節點tail開始 for (Node t = tail, p = t;;) { Node n, u; // 隊列爲空則將節點S設置爲head if (p == null && (p = head) == null) { if (casHead(null, s)) return s; } // 如果爲data else if (p.cannotPrecede(haveData)) return null; // 不是最後一個節點 else if ((n = p.next) != null) p = p != t && t != (u = tail) ? (t = u) : (p != n) ? n : null; // CAS失敗,一般來說失敗的原因在於p.next != null,可能有其他增加了tail,向前推薦 else if (!p.casNext(null, s)) p = p.next; // re-read on CAS failure else { if (p != t) { // update if slack now >= 2 while ((tail != t || !casTail(t, s)) && (t = tail) != null && (s = t.next) != null && // advance and retry (s = s.next) != null && s != t); } return p; } } }
tryAppend方法是將S節點添加到tail上,然後返回其前驅節點。好吧,我承認這段代碼我看的有點兒暈!!!
加入隊列後,如果how還不是ASYNC則調用awaitMatch()方法阻塞等待:
private E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos) { // 超時控制 final long deadline = timed ? System.nanoTime() + nanos : 0L; // 當前線程 Thread w = Thread.currentThread(); // 自旋次數 int spins = -1; // initialized after first item and cancel checks // 隨機數 ThreadLocalRandom randomYields = null; // bound if needed for (;;) { Object item = s.item; //匹配了,可能有其他線程匹配了線程 if (item != e) { // 撤銷該節點 s.forgetContents(); return LinkedTransferQueue.<E>cast(item); } // 線程中斷或者超時了。則調用將s節點item設置爲e,等待取消 if ((w.isInterrupted() || (timed && nanos <= 0)) && s.casItem(e, s)) { // cancel // 斷開節點 unsplice(pred, s); return e; } // 自旋 if (spins < 0) { // 計算自旋次數 if ((spins = spinsFor(pred, s.isData)) > 0) randomYields = ThreadLocalRandom.current(); } // 自旋 else if (spins > 0) { --spins; // 生成的隨機數 == 0 ,停止線程?不是很明白.... if (randomYields.nextInt(CHAINED_SPINS) == 0) Thread.yield(); } // 將當前線程設置到節點的waiter域 // 一開始s.waiter == null 肯定是會成立的, else if (s.waiter == null) { s.waiter = w; // request unpark then recheck } // 超時阻塞 else if (timed) { nanos = deadline - System.nanoTime(); if (nanos > 0L) LockSupport.parkNanos(this, nanos); } else { // 不是超時阻塞 LockSupport.park(this); } } }
整個awaitMatch過程和SynchronousQueue的awaitFulfill沒有很大區別,不過在自旋過程會調用Thread.yield();這是幹嘛?
在awaitMatch過程中,如果線程中斷了,或者超時了則會調用unsplice()方法去除該節點:
final void unsplice(Node pred, Node s) { s.forgetContents(); // forget unneeded fields if (pred != null && pred != s && pred.next == s) { Node n = s.next; if (n == null || (n != s && pred.casNext(s, n) && pred.isMatched())) { for (;;) { // check if at, or could be, head Node h = head; if (h == pred || h == s || h == null) return; // at head or list empty if (!h.isMatched()) break; Node hn = h.next; if (hn == null) return; // now empty if (hn != h && casHead(h, hn)) h.forgetNext(); // advance head } if (pred.next != pred && s.next != s) { // recheck if offlist for (;;) { // sweep now if enough votes int v = sweepVotes; if (v < SWEEP_THRESHOLD) { if (casSweepVotes(v, v + 1)) break; } else if (casSweepVotes(v, 0)) { sweep(); break; } } } } } }
主體流程已經完成,這裏總結下:
無論是入對、出對,還是交換,最終都會跑到xfer(E e, boolean haveData, int how, long nanos)方法中,只不過傳入的how不同而已
如果隊列不爲空,則嘗試在隊列中尋找是否存在與該節點相匹配的節點,如果找到則將匹配節點的item設置e,然後喚醒匹配節點的waiter線程。如果是reservation則叫人收貨,data則叫null收貨
如果隊列爲空,或者沒有找到匹配的節點且how != NOW,則調用tryAppend()方法將節點添加到隊列的tail,然後返回其前驅節點
如果節點的how != NOW && how != ASYNC,則調用awaitMatch()方法阻塞等待,在阻塞等待過程中和SynchronousQuque的awaitFulfill()邏輯差不多,都是先自旋,然後判斷是否需要自旋,如果中斷或者超時了則將該節點從隊列中移出
實例
這段摘自JAVA 1.7併發之LinkedTransferQueue原理理解。感覺看完上面的源碼後,在結合這個例子會有更好的瞭解,掌握。
1:Head->Data Input->Data
Match: 根據他們的屬性 發現 cannot match ,因爲是同類的
處理節點: 所以把新的data放在原來的data後面,然後head往後移一位,Reservation同理
HEAD=DATA->DATA
2:Head->Data Input->Reservation (取數據)
Match: 成功match,就把Data的item變爲reservation的值(null,有主了),並且返回數據。
處理節點: 沒動,head還在原地
HEAD=DATA(用過)
3:Head->Reservation Input->Data(放數據)
Match: 成功match,就把Reservation的item變爲Data的值(有主了),並且叫waiter來取
處理節點: 沒動
HEAD=RESERVATION(用過)
總結
BlockingQueue
BlockingQueue接口實現Queue接口,它支持兩個附加操作:獲取元素時等待隊列變爲非空,以及存儲元素時等待空間變得可用。相對於同一操作他提供了四種機制:拋出異常、返回特殊值、阻塞等待、超時:
BlockingQueue常用於生產者和消費者場景。
JDK 8 中提供了七個阻塞隊列可供使用(上圖的DelayedWorkQueue是ScheduledThreadPoolExecutor的內部類):
ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。
LinkedBlockingQueue :一個由鏈表結構組成的×××阻塞隊列。
PriorityBlockingQueue :一個支持優先級排序的×××阻塞隊列。
DelayQueue:一個使用優先級隊列實現的×××阻塞隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。
LinkedTransferQueue:一個由鏈表結構組成的×××阻塞隊列。
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
ArrayBlockingQueue
基於數組的阻塞隊列,ArrayBlockingQueue內部維護這一個定長數組,阻塞隊列的大小在初始化時就已經確定了,其後無法更改。
採用可重入鎖ReentrantLock來保證線程安全性,但是生產者和消費者是共用同一個鎖對象,這樣勢必會導致降低一定的吞吐量。當然ArrayBlockingQueue完全可以採用分離鎖來實現生產者和消費者的並行操作,但是我認爲這樣做只會給代碼帶來額外的複雜性,對於性能而言應該不會有太大的提升,因爲基於數組的ArrayBlockingQueue在數據的寫入和讀取操作已經非常輕巧了。
ArrayBlockingQueue支持公平性和非公平性,默認採用非公平模式,可以通過構造函數設置爲公平訪問策略(true)。
PriorityBlockingQueue
PriorityBlockingQueue是支持優先級的×××隊列。默認情況下采用自然順序排序,當然也可以通過自定義Comparator來指定元素的排序順序。
PriorityBlockingQueue內部採用二叉堆的實現方式,整個處理過程並不是特別複雜。添加操作則是不斷“上冒”,而刪除操作則是不斷“下掉”。
DelayQueue
DelayQueue是一個支持延時操作的×××阻塞隊列。列頭的元素是最先“到期”的元素,如果隊列裏面沒有元素到期,是不能從列頭獲取元素的,哪怕有元素也不行。也就是說只有在延遲期滿時才能夠從隊列中去元素。
它主要運用於如下場景:
緩存系統的設計:緩存是有一定的時效性的,可以用DelayQueue保存緩存的有效期,然後利用一個線程查詢DelayQueue,如果取到元素就證明該緩存已經失效了。
定時任務的調度:DelayQueue保存當天將要執行的任務和執行時間,一旦取到元素(任務),就執行該任務。
DelayQueue採用支持優先級的PriorityQueue來實現,但是隊列中的元素必須要實現Delayed接口,Delayed接口用來標記那些應該在給定延遲時間之後執行的對象,該接口提供了getDelay()方法返回元素節點的剩餘時間。同時,元素也必須要實現compareTo()方法,compareTo()方法需要提供與getDelay()方法一致的排序。
SynchronousQueue
SynchronousQueue是一個神奇的隊列,他是一個不存儲元素的阻塞隊列,也就是說他的每一個put操作都需要等待一個take操作,否則就不能繼續添加元素了,有點兒像Exchanger,類似於生產者和消費者進行交換。
隊列本身不存儲任何元素,所以非常適用於傳遞性場景,兩者直接進行對接。其吞吐量會高於ArrayBlockingQueue和LinkedBlockingQueue。
SynchronousQueue支持公平和非公平的訪問策略,在默認情況下采用非公平性,也可以通過構造函數來設置爲公平性。
SynchronousQueue的實現核心爲Transferer接口,該接口有TransferQueue和TransferStack兩個實現類,分別對應着公平策略和非公平策略。接口Transferer有一個tranfer()方法,該方法定義了轉移數據,如果e != null,相當於將一個數據交給消費者,如果e == null,則相當於從一個生產者接收一個消費者交出的數據。
LinkedTransferQueue
LinkedTransferQueue是一個由鏈表組成的的×××阻塞隊列,該隊列是一個相當牛逼的隊列:它是ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、×××的LinkedBlockingQueues等的超集。
與其他BlockingQueue相比,他多實現了一個接口TransferQueue,該接口是對BlockingQueue的一種補充,多了tryTranfer()和transfer()兩類方法:
tranfer():若當前存在一個正在等待獲取的消費者線程,即立刻移交之。 否則,會插入當前元素e到隊列尾部,並且等待進入阻塞狀態,到有消費者線程取走該元素
tryTranfer(): 若當前存在一個正在等待獲取的消費者線程(使用take()或者poll()函數),使用該方法會即刻轉移/傳輸對象元素e;若不存在,則返回false,並且不進入隊列。這是一個不阻塞的操作
LinkedBlockingDeque
LinkedBlockingDeque是一個有鏈表組成的雙向阻塞隊列,與前面的阻塞隊列相比它支持從兩端插入和移出元素。以first結尾的表示從對頭操作,以last結尾的表示從對尾操作。
在初始化LinkedBlockingDeque時可以初始化隊列的容量,用來防止其再擴容時過渡膨脹。另外雙向阻塞隊列可以運用在“工作竊取”模式中。
更多內容請關注微信公衆號【Java技術江湖】