1. 程式人生 > >Java核心技術36講 - 學習筆記

Java核心技術36講 - 學習筆記

第9講 對比Hashtable、HashMap、TreeMap有什麼不同?

一、主要不同點

資料結構 底層實現 執行緒安全 效能 支援null鍵值
HashTable 基於雜湊表 put/get/remove-o(1) 不支援
HashMap 基於雜湊表 put/get/remove-o(1) 支援
TreeMap 基於紅黑樹 put/get/remove-o(log n) 不支援null鍵,支援null值
ConcurrentHashMap 鎖分段技術 併發環境下優於同步版本的集合 不支援

第14講 談談你知道的設計模式?

一、按照模式的應用目標分類

  1. 建立型模式,解決物件建立的各種問題,如工廠、單例、構建器(Builder)、原型;
  2. 結構型模式,是對軟體設計結構的總結,關注於類、物件繼承、組合方式的實踐經驗,如裝飾者、外觀、組合、介面卡、橋接、代理;
  3. 行為型模式,是從類和物件之間互動、職責劃分等方面總結的模式,如策略、觀察者、命令、迭代器、模板方法、訪問者。

二、JDK中典型應用

JDK的IO框架運用了裝飾者模式(特徵:一系列的類以相同的抽象類或者介面作為其建構函式的入參)。如:InputStream <- FilterInputStream <- BufferedInputStream。
GUI、Swing等的元件事件監聽,運用了觀察者模式。
新版JDK中HTTP/2 Client API,建立HttpRequest,運用了構建器模式,通常被實現成fluent風格的API,也叫方法鏈。

三、Spring中的應用

  • BeanFactory和ApplicationContext應用了工廠模式
  • 在bean的建立過程中,spring為不同scope定義的的物件,提供了單例和原型等模式實現
  • AOP裡應用了裝飾者、代理、介面卡模式
  • 各種事件監聽器,是典型的觀察者模式
  • 類似JdbcTemplate運用了模板模式

第15講 synchronized和ReentrantLock有什麼區別呢?

一、兩者的區別

  • synchronized是JDK1.5之前僅有的同步手段
  • 前者可以用來修飾方法和程式碼塊,後者通過物件呼叫lock和unlock等方法的形式來使用,書寫更為靈活
  • 可再入的意思就是,一個執行緒如果已經獲取並正持有這個鎖,再次獲取時可以直接成功,這意味著獲取鎖的粒度是執行緒,而不是方法呼叫。java鎖實現強調可再入性,就是為了跟pthread(POSIX執行緒)的行為進行區分
  • 後者可以實現公平性(fairness),但是公平性在很多場景下並不是很有必要,反而會引入額外開銷,造成吞吐量的下降
  • 後者可以利用定義條件
  • 兩者的效能不能一概而論,早期版本synchronized在大部分場景下效能更差,後續版本做了很多改進,在低競爭場景中表現可能優於ReentrantLock。

二、什麼是執行緒安全

執行緒安全是一個多執行緒環境下正確性的概念,即要維持多執行緒環境下共享的、可修改的狀態的正確性。因此可以通過封裝狀態,或者讓狀態不可變(final, immutable)來保證狀態的執行緒安全。
執行緒安全需要保證幾個基本特性:

  • 原子性,相關的操作之間,某個操作不會中途被另外的執行緒干擾,一般通過同步機制實現
  • 可見性,對某個共享狀態的修改,可以立即被其他執行緒感知到,通常被解釋為將執行緒的本地狀態反映到主記憶體上,volatile就是用於保證變數的可見性
  • 有序性,指保證執行緒的序列語義,底層指令不會被重排序

三、鎖的使用

為了保證鎖的正確釋放,每一個lock()方法後最好接一個try-catch-finally塊:

lockObj.lock();
try {
	// do something
	...
} finally {
	lockObj.unlock();
}

lock()方法最好不要放在try塊中,以免lock()時發生異常,導致鎖無故被釋放。
###四、條件變數(java.util.concurrent.Condition)
如果說ReentrantLock是synchronized的替代選擇,那麼Condition則是將wait、notify、notifyAll等晦澀難懂的操作轉化為直觀可控的物件行為。
條件變數最典型的應用就是在標準類庫的ArrayBlockingQueue等中。
首先在建構函式中通過ReentrantLock物件的newCondition方法將條件變數創建出來:

/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

兩個條件變數是從同一個再入鎖物件中創建出來的,然後再將它們應用於特定的操作中,如下面的put操作,會一直等待直到notFull條件滿足:

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

那麼notFull什麼時候會滿足呢?當然是有元素出隊的時候:

private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    notFull.signal();
    return x;
}

可以看到,通過呼叫條件變數的singal()方法,通知該條件變數的條件已滿足,從而等待執行緒可以繼續之後的行為。signal和await方法要成對呼叫,不然如果只有await方法,執行緒會一直等待直到被中斷(interrupt)。

第16講 | synchronized底層如何實現?什麼是鎖的升級、降級?

一、典型回答

synchronized程式碼塊是由一對monitorenter/monitorexit指令實現的,Monitor物件是同步的基本實現單元。
在 Java 6 之前,Monitor的實現完全是靠作業系統內部的互斥鎖,因為需要進行使用者態到核心態的切換,所以同步操作是一個無差別的重量級操作。
現代JDK中,JVM對此進行了很大的改進,提供了三種不同的Monitor實現,即常說的三種不同的鎖:偏斜鎖(Biased Locking),輕量級鎖和重量級鎖,大大改進了其效能。
所謂鎖的升級、降級,就是JVM優化synchronized執行的機制,當JVM檢測到不同的競爭狀況時,會自動在這三種鎖之間切換。
當沒有競爭出現時,會預設使用偏斜鎖。JVM會使用CAS操作,在物件頭上的Mark Word部分設定執行緒ID,以表示這個物件向該執行緒傾斜,所以這裡不涉及真正的互斥鎖。這樣做的假設是基於很多場景中,大部分物件生命週期中最多隻被一個執行緒鎖定,使用偏斜鎖就降低了無競爭狀態下的開銷。
當另一個執行緒試圖去鎖定已經被偏斜的物件,JVM會撤銷掉偏斜鎖,切換到輕量級鎖。輕量鎖依賴CAS操作Mark Word來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖,否則將進一步升級到重量級鎖。

二、Java核心類庫中其它型別的鎖

Java併發包中的各種同步工具,不僅僅是各種Lock,其它的如Semphore、CountDownLatch,甚至是早期的FutureTask等,都是基於AQS框架實現的。
先看一下類圖:
在這裡插入圖片描述
ReadWriteLock是一個單獨的介面,它代表了一對鎖,分別對應只讀和寫操作。
StampedLock是一個單獨的型別,它不支援可再入的語義,即它不是以持有鎖的執行緒為單位。
為什麼會需要讀寫鎖等其它型別的鎖呢?因為synchronized和ReentrantLock都太過“霸道”,要麼不佔,要麼獨佔。在寫操作不多,只有大量併發讀操作的環境下,這些鎖的效率會比較低。
下面是一個基於ReadWriteLock實現的Map資料結構,當資料量大,併發讀多、併發寫少時,比同步版本更具優勢:

public class ReadWriteLockSample {

    private final Map<String, String> map = new HashMap<String, String>();
    private final ReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock readLock = rwl.readLock();
    private final Lock writeLock = rwl.writeLock();

    public String get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public void put(String key, String value) {
        writeLock.lock();
        try {
            map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
    
    // ...
}

在執行中,如果寫鎖已經被某個執行緒鎖定,則試圖鎖定讀鎖的操作不會成功,會等待寫鎖的釋放。
讀寫鎖看起來粒度更細一些,但在實際應用中,其表現也不盡如人意,主要是因為其相對較高的開銷。
所以JDK在後期引入了StampedLock,在提供類似讀寫鎖的同時,還支援優化讀模式,該模式基於一種假設,即大部分的讀操作都不會與寫操作衝突,其邏輯是先試著讀,然後再通過validate方法確認當時是否進入了寫模式,如果沒有,則成功避免了開銷;如果有,則重新嘗試獲取讀鎖。樣例程式碼如下:

public class StampedLockSample {

    private final StampedLock sl = new StampedLock();

    void mutate() {
        long stamp = sl.writeLock();
        try {
            write();
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    Data access() {
        long stamp = sl.tryOptimisticRead();
        Data data = read();
        if (!sl.validate(stamp)) {
            stamp = sl.readLock();
            try {
                data = read();
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return data;
    }
    
    // ...
}

注意這裡的writeLock和unlockWrite一定要保證成對呼叫。

思考:自旋鎖是什麼,適合什麼場景呢?
是低併發,且同步程式碼耗時較短時的一種樂觀的優化。

第17講 | 一個執行緒兩次呼叫start()方法會出現什麼情況?

一、典型回答

第二次呼叫start方法時,會丟擲IllegalThreadStateException,這是一種執行時異常,多次呼叫start被認為是程式設計錯誤。
Java 5 之後,執行緒的狀態被定義在其公共內部列舉類java.lang.Thread.State中,分別是:

  • 新建(NEW),執行緒被創建出來,還沒有啟動的狀態。
  • 就緒(RUNNABLE),執行緒已經在JVM中執行,可能正在執行,也可能正在等待CPU資源,在就緒佇列中排隊。
  • 阻塞(BLOCKED),與前兩講介紹的同步操作很相關,執行緒在等待某個Monitor lock。如進入synchronized程式碼塊時被阻塞。
  • 等待(WAITED),在等待其他執行緒完成某個操作,如生產者消費者模式中,消費者在消費時,如果沒有事物可消費,則會等待(wait)生產者完成生產,用類似nofity等動作通知消費者。Thread.join操作也會導致等待。
  • 計時等待(TIMED_WIATED),其進入條件和WAITED類似,但呼叫的是帶有超時條件的方法,如wait或join方法的指定超時版本,如:
public final native void wait(long timeout) throws InterruptedException;
  • 終止(TERMINATED),執行緒執行結束後的狀態。
    在第二次呼叫start時,執行緒可能處於任何非NEW的狀態,不論如何,執行緒是不允許被再次啟動的。

二、執行緒的一些基本概念

從作業系統的角度看,執行緒是系統排程的最小單元,作為任務的真正執行者,有自己的棧(Stack)、暫存器(Register)、本地儲存(Thread Local)等,但是會和程序內其它執行緒共享檔案描述符、虛擬地址空間等。
在具體實現中,執行緒還分為核心執行緒、使用者執行緒,Java執行緒的實現和虛擬機器相關,基本上在 Java 1.2 之後,JDK已經拋棄了所謂的Green Thread,也就是使用者除錯的執行緒,現在的模型是一對一對映到作業系統核心執行緒。
看Thread的原始碼,可以發現很多操作都是以JNI形式呼叫的原生代碼。

private native void start0();
private native void setPriority0(int newPriority);
private native void interrupt0();

這種實現有利有弊,總體來說,Java得益於精細粒度的執行緒和相關的併發操作,其構建高拓展性的的大型應用的能力毋庸置疑,但是它的複雜性也提高了併發程式設計的門檻。近幾年的Go語言提供了協程(coroutine),大大提高了構建併發應用的效率。於此同時,Java也在Loom專案中,孕育新的類似輕量級使用者執行緒(Fiber)等機制,將來的新版JDK中也許就會使用到它。
使用執行緒可以擴充套件Thread類,然後例項化。但更常見的做法是實現一個Runnable,將邏輯放在這個Runnable中,通過它構建Thread並啟動。這樣做的好處是,不會受Java不支援多繼承的限制,重用程式碼實現,當需要重複執行相同的程式碼時優勢明顯。而且,它也能更好地與現代Java庫中的Executor框架相結合,這樣我們不需要操心執行緒的建立和管理,也能利用Future等機制更好地處理執行結果。執行緒生命週期通常和業務之間沒有本質聯絡,混淆實現需求和業務需求,就會降低開發的效率。
下圖是執行緒狀態和方法之間的關係圖:
在這裡插入圖片描述
Thread和Object的方法,聽起來簡單,實際應用中被證明非常晦澀、易錯,這也是為什麼Java後來引入了併發包的原因。有了併發包,大多數情況下,我們都不需要直接去呼叫wait/notify之類的方法了。

三、執行緒API使用注意事項

  • 守護執行緒:有時候應用需要一個長期駐留後臺的執行緒,但又不希望其影響應用退出,就可以將其設定為守護執行緒,這個設定必須在啟動執行緒之間完成。
Thread daemonThread = new Thread();
daemonThread.setDaemon(true);
daemonThread.start();
  • Spurious wakeup:尤其是在多核CPU的系統中,執行緒等待存在一種問題,就是在沒有任務通知或廣播發出的情況下,執行緒就被喚醒。如果處理不當,就會發生詭異的併發問題,所以我們在等待條件過程中,建議採用下述的方式來書寫程式碼:
// 推薦
while (isCondition()) {
    waitForCondition(...);
}
// 不推薦,可能引入bug
if (isCondition()) {
    waitForCondition(...);
}
  • Thread.onSpinWait():這是 Java 9 引入的特性。16講的思考題中提到了自旋鎖,也可以認為它不是一種鎖,而是針對短期等待的一種效能優化技術。onSpinWait()沒有任何行為上的保證,而是對JVM的一個暗示,JVM可能通過CPU的pause指令進一步提高效能,效能特別敏感的應用可以關注。
  • 慎用ThreadLocal:這是Java提供的一種儲存執行緒私有資訊的機制,因為其線上程整個生命週期中都有效,所以可以方便地在某個執行緒相關的業務模組之間傳遞資訊,如Cookie、事務ID等上下文資訊。
    它的資料儲存於執行緒相關的ThreadLocalMap中,其內部條目是弱引用。當Key為null時,該條目就變為廢棄條目,相關value的回收,往往依賴於幾個關鍵點,即set、remove、rehash。通常弱引用都會和引用佇列配合清理機制使用,但ThreadLocalMap並沒有這樣做,這意味著,廢棄專案的回收依賴於顯式的觸發,否則就要等待執行緒結束,進而回收相應ThreadLocalMap,這是很多OOM的來源。所以通常建議,應用一定要自己負責remove,並且不要和執行緒池配合,因為worker執行緒往往是不會退出的。

第18講 | 什麼情況下Java程式會產生死鎖?如何定位、修復?

一、典型回答

死鎖是一種特定的程式狀態。在多個實體之間,由於迴圈依賴,導致彼此一直處於等待之中,沒有個體能夠繼續前進。死鎖不光發生線上程之間,存在資源獨佔的程序之間也可能發生死鎖。
定位死鎖最常見的方法就是利用jstack等工具獲取執行緒棧,然後定位互相之間的依賴關係,進而找到死鎖。如果是比較明顯的死鎖,往往jstack就能直接發現問題所在,類似JConsole甚至可以在圖形介面進行有限的死鎖檢測。
用jstack檢測死鎖的步驟為,首先通過jps或系統的ps命令、工作管理員等找到程式的程序ID,其實通過jstack pid命令獲取執行緒棧,最後結合程式碼分析執行緒棧資訊,找出死鎖。如果是簡單的死鎖,jstack可以直接替我們找出:
在這裡插入圖片描述
上圖明顯告訴了我們存在死鎖,以及死鎖的成因。
如果程式執行過程中發生了死鎖,往往是無法線上解決的。只能通過重啟、修改程式來解決問題。所以程式碼開發階段互相審查,或者利用工具進行預防性排查,也是很重要的。

二、預防死鎖的方法

死鎖發生的四個必要條件為:

  1. 互斥
  2. 請求與保持
  3. 不可剝奪
  4. 環路等待

可以破壞上述條件之一,破壞掉任意一個即可解除死鎖。互斥和不可剝奪的條件在併發環境下可能不太好破壞,畢竟要保證執行緒安全。
下面是幾種預防死鎖的方法:

  • 如果可能,儘量避免使用多個鎖,並且只有需要時才持有鎖。這是破壞了請求與保持的條件。
  • 如果必須使用多個鎖,儘量設計好獲取鎖的順序,這一點知易行難,可以參看著名的銀行家演算法。這是破壞了環路等待的條件。
  • 使用帶超時的方法,為程式帶來更多可控性。類似Object.wait(…)或CountDownLatch.await(…),都支援所謂的TIMED_WAIT,我們完全可以假定該鎖不一定會獲得,指定超時時間,設計好超時無法獲取時的邏輯。這也是破壞了請求與保持的邏輯。
  • 業界也有一些其它方面的嘗試,如通過靜態程式碼分析(如FindBugs)去查詢固定的模式,進而發現可能存在的死鎖或競爭的狀況。實踐證明這種方法也有一定的作用,可參考相關文件
    除了上述典型場景的死鎖,還有一些更令人頭疼的死鎖,如類載入過程中發生的死鎖,尤其是框架大量使用自定義類載入時,因為往往不是在應用本身的程式碼庫中,所以使用jstack等工具也不見得能夠顯示全部鎖資訊,所以處理起來比較棘手。對此,Java有官方文件對此進行說明,並針對特定情況提供了相應的JVM引數和基本準則。

三、思考題

關於今天我們討論的題目你做到心中有數了嗎?今天的思考題是,有時候並不是阻塞導致的死鎖,只是某個執行緒進入了死迴圈,導致其他執行緒一直等待,這種問題如何診斷呢?
這種情況可以認為是自旋鎖死鎖的一種,其它執行緒因為得不到具體的訊號提示,導致執行緒一直飢餓。這種情況下可以檢視執行緒CPU的使用情況,排查出使用CPU時間片最多的執行緒,再找出該執行緒的堆疊資訊,排查程式碼。
基於互斥量的鎖如果發生死鎖,往往CPU的使用率較低,實踐中也可以從這方面進行排查。

第19講 | Java併發包提供了哪些併發工具類?

一、典型回答

我們通常說的Java併發包就是java.util.concurrent及其子包,包含了java的各種基礎併發工具類,具體包括以下幾個方面:

  • 比synchronized更加高階的各種用於同步的結構,如Semphore, CountDownLatch, CyclicBarrier等,可以實現更加豐富的多執行緒操作。
  • 執行緒安全的容器,如提高併發效能的ConcurrentHashMap、有序的ConcurrentSkipListMap,或者通過類似快照機制,實現執行緒安全的動態陣列CopyOnWriteArrayList等。
  • 各種併發佇列的實現,如各種BlockingQueue的實現,比較典型的ArrayBlockingQueue、SynchronousQueue,以及針對特定場景的PriorityQueue等。
  • 強大的Executor框架,可以建立各種型別的執行緒池,進行任務排程,絕大多數情況下,不需要自己從頭實現執行緒池和任務排程器。

我們進行多執行緒程式設計,無非是達到這樣幾個目的:

  • 進行多個執行緒之間的排程、協作,以完成業務邏輯。
  • 線上程之間傳遞資料和狀態,這同樣是為了業務需要。
  • 實現程式的高擴充套件性,以達到業務對吞吐量的需求。

二、CountDownLatch和CyclicBarrier的區別

  • CountDownLatch是不可重置的,所以無法重用;CyclicBarrier可以重用。
  • CountDownLatch的基本操作組合是countDown/await,呼叫await操作的執行緒等待countDown執行足夠多的次數,不管是多個執行緒來執行countDown,還是一個執行緒執行多次countDown,只要次數足夠即可。所以可以說CountDownLatch操作的是事件。
  • CyclicBarrier的基本操作就是await,當所有的夥伴(parties)都執行了await,大家才會繼續向前走,並自動進行重置。正常情況下,重置是自動發生的,如果我們呼叫reset方法,但還有執行緒在等待,就會導致等待執行緒被打擾,丟擲BrokenBarrierException異常。CyclicBarrier的側重點是執行緒,而不是呼叫事件。它的典型應用場景是等待併發執行緒結束。

三、執行緒安全容器

首先可參考下面的類圖:
在這裡插入圖片描述
總體上類的結構比較簡單。如果我們側重於Map放入或獲取的速度,而不在乎順序,那麼應該選ConcurrentHashMap,否則選ConcurrentSkipListMap;如果我們要對大量資料進行頻繁的修改,那麼ConcurrentSkipListMap也可能表現出優勢。
為什麼併發容器裡沒有ConcurrentTreeMap呢?因為要紅黑樹在插入、刪除結點時,都要移動樹的節點從而達到平衡,這導致在多執行緒場景下很難進行合適粒度的同步,所以很難實現高效的執行緒安全。
而SkipListMap結構則簡單很多,通過層次結構提高訪問速度,雖然空間不夠緊湊(O(nlogn)),但是在增刪元素時執行緒安全的開銷要小很多。下面是它的結構示意圖:
在這裡插入圖片描述
關於兩個CopyOnWrite容器,其實CopyOnWriteArraySet是包裝了CopyOnWriteArrayList來實現的,所以在學習時可以專注其中一種。
CopyOnWrite的意思是,任何修改操作,如add, remove, set,都會導致陣列的複製,對複製的陣列進行修改後,再直接替換掉原來的陣列,通過這種防禦性的方式,來實現另類的執行緒安全。所以這種資料結構,還是適合讀多寫少的場景,不然修改的開銷是比較明顯的。

第20講 | 併發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什麼區別?

一、典型回答

  • ConcurrentLinkedQueue基於lock-free,在高併發場景下具有較高的效能。
  • LinkedBlockingQueue內部基於鎖,提供了BlockingQueue的特性方法。

Java併發包中的容器,從命名上看可大致分為三類:Concurrent*, CopyOnWrite*和Blocking,同樣是執行緒安全容器,它們的區別為:

  • Concurrent容器沒有CopyOnWrite容器的高修改開銷。
  • 凡事都有兩面性,Concurrent容器往往提供了較低的遍歷一致性,可以這樣理解所謂的弱一致性,即迭代器在遍歷時,容器如果發生了修改,遍歷還可以繼續。
  • 弱一致性的另一個特性是size()操作不一定準確。
  • 與此同時,弱一致性的容器,讀取的效能具有一定的不確定性。
  • 與弱一致性對應的是同步容器常有的屬性“fail-fast”,也就是在遍歷過程中容器如果發生了修改,則會丟擲ConcurrentModificationException,不再繼續遍歷。

二、執行緒安全佇列概述

廢話不多說,先上圖,圖中沒有將非執行緒安全佇列包括進來:
在這裡插入圖片描述
Deque型別的側重點是對佇列頭尾都支援插入、刪除操作。
大部分型別實現了BlockingQueue介面,意思就是在插入時如果有必要會等待直到佇列不滿,獲取時同樣會等待直到佇列非空。
另一個BlockingQueue常被考察的點是佇列是否有界,這一點也往往會影響我們在應用開發時的選擇,簡單總結如下:

  • ArrayBlockingQueue是最典型的有界佇列,其內部以final的陣列儲存資料,建立佇列時就要指定容量,如
public ArrayBlockingQueue(int capacity, boolean fair)
  • LinkedBlockingQueue,容易被誤解為無界佇列,但是其行為和內部程式碼是基於有界的邏輯實現的,在初始化時也要指定容量,如果沒有指定,則預設是Interger.MAX_VALUE,即變成了無界佇列。
  • SynchronousQueue是一個比神奇的佇列實現,它的每個刪除操作都要等待插入操作,反之亦然。它的容量是1嗎?No,是0。
  • PriorityQueue是無界佇列,雖然從嚴格意義上講,它的容量也要受系統資源限制。
  • DelayedQueue和LinkedTransferQueue也是無邊界佇列。對於無界佇列,有一個很自然的屬性就是插入操作永遠不需要等待。

如果我們分析不同佇列的底層實現,BlockingQueue的內部基本都是基於鎖實現,下面是典型的LinkedBlockingQueue:

/** 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();

可以看出,和之前介紹過的ArrayBlockingQueue不同的是,LinkedBlockingQueue內部的兩個條件變數,是從兩個不同的ReentrantLock中構建出來的,粒度更細,所以在通用場景下,LinkedBlockingQueue的吞吐量要大於Array的。
下面的take方法與ArrayBlockingQueue也不一樣,因為是連結串列結構,它要自己維護佇列的元素數量值:

public E take() throws InterruptedException {
    final E x;
    final int c;
    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)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

而類似ConcurrentLinkedQueue,則是基於CAS的無鎖技術,不需要在每個操作時使用鎖,所以擴充套件性表現要更加優異。
SynchronousQueue,在 Java 6 中,其實現方式發生了很大的變化,由CAS操作代替了之前基於鎖的邏輯,是Executors.newCachedThreadPool()預設使用的佇列。

三、日常開發中如何選擇佇列

以LinkedBlockingQueue、ArrayBlockingQueue和SynchronousQueue為例,需求可以從多個方面來考慮:

  • 考慮應用場景中對邊界的要求,ArrayBlockingQueue是有明確的容量限制的,而LinkedBlockingQueue的容量限制取決於我們在建立時是否指定,SynchronousQueue則不能快取任何元素。
  • 從空間利用角度來看,ArrayBlockingQueue對空間的利用更加緊湊,因為它使用的是陣列,不需要建立所謂節點,但因為要申請連續的記憶體空間,所以初始化時對空間的要求更高。
  • 通用場景下,LinkedBlockingQueue的吞吐量一般比ArrayBlockingQueue更高,因為其使用了更加細粒度的鎖操作。
  • ArrayBlockingQueue實現更加簡單,效能更好預測,屬於表現穩定的選手。
  • 如果我們需要實現的兩個執行緒接力性(handoff)的場景,SynchronousQueue則很適合這種場景,而且執行緒間協調和資料傳輸統一起來,程式碼更加規範。
  • 很多時候SynchronousQueue的效能表現往往大大超過其它實現,尤其是在佇列元素較小的場景。