Java面試進階:併發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什麼區別?

答:

Concurrent 類型基於 lock-free,基於CAS的無鎖技術,在常見的多線程訪問場景,一般可以提供較高吞吐量。

而 LinkedBlockingQueue 內部則是基於鎖,並提供了 BlockingQueue 的等待性方法。

 

,java.util.concurrent 包提供的容器

大概區分爲 Concurrent*、CopyOnWrite和 Blocking等三類

Concurrent 類型沒有類似 CopyOnWrite 之類容器相對較重的修改開銷。Concurrent 往往提供了較低的遍歷一致性。即的弱一致性,當利用迭代器遍歷時,如果容器發生修改,迭代器仍然可以繼續進行遍歷,且弱一致性,size 等操作準確性是有限的,未必是 100% 準確。讀取的性能具有一定的不確定性。

與弱一致性對應的,就是我介紹過的同步容器常見的行爲“fail-fast”,也就是檢測到容器在遍歷過程中發生了修改,則拋出 ConcurrentModificationException,不再繼續遍歷。


Deque 的側重點是支持對隊列頭尾都進行插入和刪除,尾部插入時需要的addLast(e)、offerLast(e)。尾部刪除所需要的removeLast()、pollLast()。

Blocking 意味着其提供了特定的等待性操作,獲取時(take)等待元素進隊,或者插入時(put)等待隊列出現空位。

 

哪些隊列是有界的,哪些是無界的:

ConcurrentLinkedQueue是一個基於鏈接節點的無界線程安全隊列

ArrayBlockingQueue 是最典型的的有界隊列,其內部以 final 的數組保存數據,數組的大小就決定了隊列的邊界,所以我們在創建 ArrayBlockingQueue 時,都要指定容量。

LinkedBlockingQueue,容易被誤解爲無邊界,但其實其行爲和內部代碼都是基於有界的邏輯實現的,只不過如果我們沒有在創建隊列時就指定容量,那麼其容量限制就自動被設置爲 Integer.MAX_VALUE,成爲了無界隊列。

SynchronousQueue,這是一個非常奇葩的隊列實現,每個刪除操作都要等待插入操作,反之每個插入操作也都要等待刪除動作。那麼這個隊列的容量是多少呢?是 1 嗎?其實不是的,其內部容量是 0。

PriorityBlockingQueue 是無邊界的優先隊列,雖然嚴格意義上來講,其大小總歸是要受系統資源影響。

DelayedQueue 和 LinkedTransferQueue 同樣是無邊界的隊列。對於無邊界的隊列,有一個自然的結果,就是 put 操作永遠也不會發生其他 BlockingQueue 的那種等待情況。

 

SynchronousQueue,在 Java 6 中,其實現發生了非常大的變化,利用 CAS 替換掉了原本基於鎖的邏輯,同步開銷比較小。它是 Executors.newCachedThreadPool() 的默認隊列。

Queue 被廣泛使用在生產者 - 消費者場景,BlockingQueue 來實現,由於其提供的等待機制,我們可以少操心很多協調工作

生產者消費者模式:


import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ConsumerProducer {
    public static final String EXIT_MSG  = "Good bye!";
    public static void main(String[] args) {
// 使用較小的隊列,以更好地在輸出中展示其影響
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);
        new Thread(producer).start();
        new Thread(consumer).start();
    }


    static class Producer implements Runnable {
        private BlockingQueue<String> queue;
        public Producer(BlockingQueue<String> q) {
            this.queue = q;
        }

        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                try{
                    Thread.sleep(5L);
                    String msg = "Message" + i;
                    System.out.println("Produced new item: " + msg);
                    queue.put(msg);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            try {
                System.out.println("Time to say good bye!");
                queue.put(EXIT_MSG);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Consumer implements Runnable{
        private BlockingQueue<String> queue;
        public Consumer(BlockingQueue<String> q){
            this.queue=q;
        }

        @Override
        public void run() {
            try{
                String msg;
                while(!EXIT_MSG.equalsIgnoreCase( (msg = queue.take()))){
                    System.out.println("Consumed item: " + msg);
                    Thread.sleep(10L);
                }
                System.out.println("Got exit message, bye!");
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

實際開發中如何選擇這些隊列:

對比

queue 阻塞與否 是否有界 線程安全保障 適用場景 注意事項
ArrayBlockingQueue 阻塞 有界 一把全局鎖 生產消費模型,平衡兩邊處理速度 --
LinkedBlockingQueue 阻塞 可配置 存取採用2把鎖 生產消費模型,平衡兩邊處理速度 無界的時候注意內存溢出問題
ConcurrentLinkedQueue 非阻塞 無界 CAS 對全局的集合進行操作的場景 size() 是要遍歷一遍集合,慎用

考慮應用場景中對隊列邊界的要求。ArrayBlockingQueue 是有明確的容量限制的,而 LinkedBlockingQueue 則取決於我們是否在創建時指定,SynchronousQueue 則乾脆不能緩存任何元素。

空間利用角度,數組結構的 ArrayBlockingQueue 要比 LinkedBlockingQueue 緊湊,因爲其不需要創建所謂節點,但是其初始分配階段就需要一段連續的空間,所以初始內存需求更大。

通用場景中,LinkedBlockingQueue 的吞吐量一般優於 ArrayBlockingQueue,因爲它實現了更加細粒度的鎖操作。ArrayBlockingQueue 實現比較簡單,性能更好預測,屬於表現穩定的“選手”。

 SynchronousQueue 的性能表現,往往大大超過其他實現,尤其是在隊列元素較小的場景,還可代替CountDownLatch實現的是兩個線程之間接力性(handoff)的場景。

 

指定某種結構,比如棧,用它實現一個 BlockingQueue,實現思路是怎樣的呢?

總共3個棧,其中2個寫入棧(A、B),1個消費棧棧C(消費數據),但是有1個寫入棧是空閒的棧(B),隨時等待寫入,當消費棧(C)中數據爲空的時候,消費線程(await),觸發數據轉移,原寫入棧(A)停止寫入,,由空閒棧(B)接受寫入的工作,原寫入棧(A)中的數據轉移到消費棧(C)中,轉移完成後繼續(sign)繼續消費,2個寫入棧,1個消費棧優點是:不會堵塞寫入,但是消費會有暫停

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章