1. 程式人生 > >常用阻塞佇列 BlockingQueue 有哪些?

常用阻塞佇列 BlockingQueue 有哪些?

## 為什麼要使用阻塞佇列 之前,介紹了一下 ThreadPoolExecutor 的各引數的含義([併發程式設計之執行緒池ThreadPoolExecutor](https://blog.csdn.net/qq_26542493/article/details/104547540)),其中有一個 BlockingQueue,它是一個阻塞佇列。那麼,小夥伴們有沒有想過,為什麼此處的執行緒池要用阻塞佇列呢? 我們知道佇列是先進先出的。當放入一個元素的時候,會放在佇列的末尾,取出元素的時候,會從隊頭取。那麼,當佇列為空或者佇列滿的時候怎麼辦呢。 這時,阻塞佇列,會自動幫我們處理這種情況。 當阻塞佇列為空的時候,從佇列中取元素的操作就會被阻塞。當阻塞佇列滿的時候,往佇列中放入元素的操作就會被阻塞。 而後,一旦空佇列有資料了,或者滿佇列有空餘位置時,被阻塞的執行緒就會被自動喚醒。 這就是阻塞佇列的好處,你不需要關心執行緒何時被阻塞,也不需要關心執行緒何時被喚醒,一切都由阻塞佇列自動幫我們完成。我們只需要關注具體的業務邏輯就可以了。 而這種阻塞佇列經常用在生產者消費者模式中。(可參看:[面試官讓我手寫一個生產者消費者模式](https://blog.csdn.net/qq_26542493/article/details/104507725)) ## 常用的阻塞佇列 那麼,一般我們用到的阻塞佇列有哪些呢。下面,通過idea的類圖,列出來常用的阻塞佇列,然後一個一個講解(不懂怎麼用的,可以參考這篇文章:[怎麼用IDEA快速檢視類圖關係](https://blog.csdn.net/qq_26542493/article/details/104512954))。 ![](https://img2020.cnblogs.com/blog/1714084/202003/1714084-20200304232931666-392170037.jpg) 阻塞佇列中,所有常用的方法都在 BlockingQueue 介面中定義。如 插入元素的方法: put,offer,add。移除元素的方法: remove,poll,take。 它們有四種不同的處理方式,第一種是在失敗時丟擲異常,第二種是在失敗時返回特殊值,第三種是一直阻塞當前執行緒,最後一種是在指定時間內阻塞,否則返回特殊值。(以上特殊值,是指在插入元素時,失敗返回false,在取出元素時,失敗返回null) || 拋異常 | 特殊值 | 阻塞 | 超時 | |-----|-----|-----|------|------| |插入| add(e) | offer(e) | put(e) | offer(e,time,unit) | |移除| remove() | poll() | take() | poll(time,unit) |
**1) ArrayBlockingQueue** 這是一個由陣列結構組成的有界阻塞佇列。首先看下它的構造方法,有三個。 ![](https://img2020.cnblogs.com/blog/1714084/202003/1714084-20200304232931942-1490220721.jpg) 第一個可以指定佇列的大小,第二個還可以指定佇列是否公平,不指定的話,預設是非公平。它是使用 ReentrantLock 的公平鎖和非公平鎖實現的(後續講解AQS時,會詳細說明)。 簡單理解就是,ReentrantLock 內部會維護一個有先後順序的等待佇列,假如有五個任務一起過來,都被阻塞了。如果是公平的,則等待佇列中等待最久的任務就會先進入阻塞佇列。如果是非公平的,那麼這五個執行緒就需要搶鎖,誰先搶到,誰就先進入阻塞佇列。 第三個構造方法,是把一個集合的元素初始化到阻塞佇列中。 另外,ArrayBlockingQueue 沒有實現讀寫分離,也就是說,讀和寫是不能同時進行的。因為,它讀寫時用的是同一把鎖,如下圖所示: ![file](https://img2020.cnblogs.com/blog/1714084/202003/1714084-20200304232932128-556715476.jpg) **2) LinkedBlockingQueue** 這是一個由連結串列結構組成的有界阻塞佇列。它的構造方法有三個。 ![](https://img2020.cnblogs.com/blog/1714084/202003/1714084-20200304232932304-2022973518.jpg) 可以看到和 ArrayBlockingQueue 的構造方法大同小異,不過是,LinkedBlockingQueue 可以不指定佇列的大小,預設值是 Integer.MAX_VALUE 。 但是,最好不要這樣做,建議指定一個固定大小。因為,如果生產者的速度比消費者的速度大的多的情況下,這會導致阻塞佇列一直膨脹,直到系統記憶體被耗盡(此時,還沒達到佇列容量的最大值)。 此外,LinkedBlockingQueue 實現了讀寫分離,可以實現資料的讀和寫互不影響,這在高併發的場景下,對於效率的提高無疑是非常巨大的。 ![](https://img2020.cnblogs.com/blog/1714084/202003/1714084-20200304232932533-880942875.jpg) **3) SynchronousQueue** 這是一個沒有緩衝的無界佇列。什麼意思,看一下它的 size 方法: ![](https://img2020.cnblogs.com/blog/1714084/202003/1714084-20200304232932750-83356586.jpg) 總是返回 0 ,因為它是一個沒有容量的佇列。 當執行插入元素的操作時,必須等待一個取出操作。也就是說,put元素的時候,必須等待 take 操作。 那麼,有的同學就好奇了,這沒有容量,還叫什麼佇列啊,這有什麼意義呢。 我的理解是,這適用於併發任務不大,而且生產者和消費者的速度相差不多的場景下,直接把生產者和消費者對接,不用經過佇列的入隊出隊這一系列操作。所以,效率上會高一些。 可以去檢視一下 Excutors.newCachedThreadPool 方法用的就是這種佇列。 這個佇列有兩個構造方法,用於傳入是公平還是非公平,預設是非公平。 ![](https://img2020.cnblogs.com/blog/1714084/202003/1714084-20200304232932985-318783051.jpg) **4)PriorityBlockingQueue** 這是一個支援優先順序排序的無界佇列。有四個構造方法: ![](https://img2020.cnblogs.com/blog/1714084/202003/1714084-20200304232933177-1034756041.jpg) 可以指定初始容量大小(注意初始容量並不代表最大容量),或者不指定,預設大小為 11。也可以傳入一個比較器,把元素按一定的規則排序,不指定比較器的話,預設是自然順序。 PriorityBlockingQueue 是基於二叉樹最小堆實現的,每當取元素的時候,就會把優先順序最高的元素取出來。我們測試一下: ``` public class Person { private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Person{" + "id=" + id + ", name='" + name + '\'' + '}'; } public Person(int id, String name) { this.id = id; this.name = name; } public Person() { } } public class QueueTest { public static void main(String[] args) throws InterruptedException { PriorityBlock