02.29 詳解java中一個面試常問的知識點-阻塞隊列

學習數據結構的時候介紹過隊列,今天介紹一種隊列的其中一種,叫做阻塞隊列。這個知識點屬於多線程中的一個模塊,對於我們理解消息中間件有份非常大的用處,希望對你有幫助。

一、什麼是阻塞隊列

1、概念理解

隊列比較好理解,數據結構中我們都接觸過,先進先出的一種數據結構,那什麼是阻塞隊列呢?從名字可以看出阻塞隊列其實也就是隊列的一種特殊情況。舉個例子來說明一下吧,我們去餐館吃飯,一個接一個的下單,這時候就是一個普通的隊列,萬一這家店生意好,餐館擠滿了人,這時候肯定不能把顧客趕出去,於是餐館就在旁邊設置了一個休息等待區。這就是一個阻塞隊列了。我們使用一張圖來演示一下:


詳解java中一個面試常問的知識點-阻塞隊列


2、特點

從上面這張圖我們會發現這樣的規律:

(1)當阻塞隊列為空時,從隊列中獲取元素的操作將會被阻塞,就好比餐館休息區沒人了,此時不能接納新的顧客了。換句話,肚子為空的時候也沒東西吃。

(2)當阻塞隊列滿了,往隊列添加元素的操作將會被阻塞,好比餐館的休息區也擠滿了,後來的顧客只能走了。

從上面的概念我們類比到線程中去,我們會發現,在某些時候線程可能不能不阻塞,因為CPU內核就那麼幾個,阻塞現狀更加說明了資源的利用率高,換句話來說,阻塞其實是一個好事。

阻塞隊列應用最廣泛的是生產者和消費者模式,待會也會給出一個實際案例演示一下。

還有一點,就是我們看這個阻塞隊列有點線程池的感覺,其實基本上可以這樣理解,阻塞隊列在線程池中確實有著很大的應用。我們可以給出倆例子:


詳解java中一個面試常問的知識點-阻塞隊列


前面說了這麼久,來個標準點的定義吧:

在多線程中,阻塞的意思是,在某些情況下會掛起線程,一旦條件成熟,被阻塞的線程就會被自動喚醒。

也就是說,之前線程的wait和notify我們程序員需要自己控制,但有了這個阻塞隊列之後我們程序員就不用擔心了,阻塞隊列會自動管理。

歐了,我們對概念先認識到這,我們從代碼中看看,畢竟面試中X疼的就是代碼。

二、常見的BlockQueue方法

BlockQueue接口繼承自collection接口。他的主要實現方法比較多。我們分類來看一下:


詳解java中一個面試常問的知識點-阻塞隊列


方法就這些,這些方法一個一個看和演示的話我覺得有點傻,參照網絡上別人的一些博客也對其進行了分類:

根據插入和取出兩種類型的操作,具體分為下面幾些類型:


詳解java中一個面試常問的知識點-阻塞隊列


  • 拋出異常:這時候插入和取出在不能立即被執行的時候就會拋出異常。
  • 特殊值:插入和取出在不能被立即執行的情況下會返回一個特殊的值(true 或者 false)
  • 阻塞:插入和取出操作在不能被立即執行時會阻塞線程,直到條件成熟,被其他線程喚醒
  • 超時:插入和取出操作在不能立即執行的時候會被阻塞一定的時候,如果在指定的時間內沒有被執行,那麼會返回一個特殊值。

單單從操作的維度來看的話,還是會有點錯,因為有些方法是阻塞方法,有些方法不是,我們從阻塞和不阻塞的維度再來一次劃分:


詳解java中一個面試常問的知識點-阻塞隊列


現在我們再來看,相信會比較清楚一點,不過需要注意一些特殊的情況,比如offer和poll,以是否包含超時時間來區分是否阻塞。

三、常見的阻塞隊列

實現了BlockQueue接口的隊列有很多,常見的沒有幾種,我們使用表格的形式給列出來,對比著分析一下:


詳解java中一個面試常問的知識點-阻塞隊列


常見的幾種已經加粗了。

ArrayBlockingQueue和LinkedBlockingQueue是最為常用的阻塞隊列,前者使用一個有邊界的數組來作為存儲介質,而後者使用了一個沒有邊界的鏈表來存儲數據。

PriorityBlockingQueue是一個優先阻塞隊列。所謂優先隊列,就是每次從隊隊列裡面獲取到的都是隊列中優先級最高的,對於優先級,PriorityBlockingQueue需要你為插入其中的元素類型提供一個Comparator,PriorityBlockingQueue使用這個Comparator來確定元素之間的優先級關係。底層的數據結構是堆,也就是我們數據結構中的那個堆。

DelayQueue是一個延時隊列,所謂延時隊列就是消費線程將會延時一段時間來消費元素。

SynchronousQueue是最為複雜的阻塞隊列。SynchronousQueue和前面分析的阻塞隊列都不同,因為SynchronousQueue不存在容量的說法,任何插入操作都需要等待其他線程來消費,否則就會阻塞等待,看到這種隊列心裡面估計就立馬能聯想到生產者消費者的這種模式了,沒錯,就可以使用這個隊列來實現。

現在,我們已經把阻塞隊列的一些基本知識點介紹了,完全帶細節的介紹費時又費力,下面我們針對某個阻塞隊列來看一下原理,其實就是看看源碼是如何實現的。

四、原理

我們以ArrayBlockingQueue為例,以下源碼均來自jdk1.8。還是以變量、構造函數、普通函數的順序來看:

1、變量


詳解java中一個面試常問的知識點-阻塞隊列


變量的作用基本上就是這樣,我們再來接著看構造函數

2、構造函數


詳解java中一個面試常問的知識點-阻塞隊列


上面的這些其實都是為了給其他操作做鋪墊。

3、put函數


詳解java中一個面試常問的知識點-阻塞隊列


首先檢查是否為空,從這個方法中我們可以看到,首先檢查隊列是否為空,然後獲取鎖,判斷當前元素個數是否等於數組的長度,如果相等,則調用notFull.await()進行等待,如果捕獲到中斷異常,則喚醒線程並拋出異常。當被其他線程喚醒時,通過enqueue(e)方法插入元素,最後解鎖。

我們按照這個源碼來看,真正實現插入操作的是enqueue,我們跟進去看看:


詳解java中一個面試常問的知識點-阻塞隊列


就幾行代碼,就是一個正常的移動數組插入的過程,不過最後還要再通知一下隊列,插入了元素,此時的隊列就不為空了。

4、take元素

還是看源碼


詳解java中一個面試常問的知識點-阻塞隊列


take的這個操作根據put反過來看就可以,真正實現的是dequeue,跟進去看看:


詳解java中一個面試常問的知識點-阻塞隊列


取出的時候也是一樣,數組少一個元素,數量少一,最後通過隊列不為空。其他的就不詳述了。

最後我們看看使用。我們舉一個生產者消費者的例子,畢竟這個是一個面試考點:

五、應用

生產者消費者模式的實現方式超級多,比如volatile、CAS、AtomicInteger等等,這次我們就使用阻塞隊列來實現一下:


詳解java中一個面試常問的知識點-阻塞隊列


驗證就比較簡單,我們新建幾個生產線程和幾個消費線程即可。在這裡就不貼代碼了。

OK,阻塞隊列基本的一些知識就是這些,如有問題還請批評指正。


分享到:


相關文章: