還不了解Java的五大BlockingQueue阻塞隊列源碼,看這篇文章就夠了
引言
最近一個月一直在更新《解讀Java源碼專欄》,其中跟大家一起剖析了Java的常見的5種BlockingQueue(阻塞隊列),今天就盤點一下這幾種阻塞隊列的優(yōu)缺點、區(qū)別,以及應(yīng)用場景。
常見的BlockingQueue有以下5種,下面會詳細介紹。
- ArrayBlockingQueue
基于數(shù)組實現(xiàn)的阻塞隊列,創(chuàng)建隊列時需指定容量大小,是有界隊列。
- LinkedBlockingQueue
基于鏈表實現(xiàn)的阻塞隊列,默認是無界隊列,創(chuàng)建可以指定容量大小
- SynchronousQueue
一種沒有緩沖的阻塞隊列,生產(chǎn)出的數(shù)據(jù)需要立刻被消費
- PriorityBlockingQueue
實現(xiàn)了優(yōu)先級的阻塞隊列,可以按照元素大小排序,是無界隊列
- DelayQueue
實現(xiàn)了延遲功能的阻塞隊列,基于PriorityQueue實現(xiàn)的,是無界隊列
BlockingQueue簡介
這幾種阻塞隊列都是實現(xiàn)了BlockingQueue接口,在日常開發(fā)中,我們好像很少用到BlockingQueue(阻塞隊列),BlockingQueue到底有什么作用?應(yīng)用場景是什么樣的?
如果使用過線程池或者閱讀過線程池源碼,就會知道線程池的核心功能都是基于BlockingQueue實現(xiàn)的。
大家用過消息隊列(MessageQueue),就知道消息隊列作用是解耦、異步、削峰。同樣BlockingQueue的作用也是這三種,區(qū)別是BlockingQueue只作用于本機器,而消息隊列相當于分布式BlockingQueue。
BlockingQueue作為阻塞隊列,主要應(yīng)用于生產(chǎn)者-消費者模式的場景,在并發(fā)多線程中尤其常用。
- 比如像線程池中的任務(wù)調(diào)度場景,提交任務(wù)和拉取并執(zhí)行任務(wù)。
- 生產(chǎn)者與消費者解耦的場景,生產(chǎn)者把數(shù)據(jù)放到隊列中,消費者從隊列中取數(shù)據(jù)進行消費。兩者進行解耦,不用感知對方的存在。
- 應(yīng)對突發(fā)流量的場景,業(yè)務(wù)高峰期突然來了很多請求,可以放到隊列中緩存起來,消費者以正常的頻率從隊列中拉取并消費數(shù)據(jù),起到削峰的作用。
BlockingQueue是個接口,定義了幾組放數(shù)據(jù)和取數(shù)據(jù)的方法,來滿足不同的場景。
操作 | 拋出異常 | 返回特定值 | 阻塞 | 阻塞一段時間 |
放數(shù)據(jù) | add() | offer() | put() | offer(e, time, unit) |
取數(shù)據(jù)(同時刪除數(shù)據(jù)) | remove() | poll() | take() | poll(time, unit) |
取數(shù)據(jù)(不刪除) | element() | peek() | 不支持 | 不支持 |
這四組方法的區(qū)別是:
- 當隊列滿的時候,再次添加數(shù)據(jù),add()會拋出異常,offer()會返回false,put()會一直阻塞,offer(e, time, unit)會阻塞指定時間,然后返回false。
- 當隊列為空的時候,再次取數(shù)據(jù),remove()會拋出異常,poll()會返回null,take()會一直阻塞,poll(time, unit)會阻塞指定時間,然后返回null。
ArrayBlockingQueue
- ArrayBlockingQueue底層基于數(shù)組實現(xiàn),采用循環(huán)數(shù)組,提升了數(shù)組的空間利用率。
- ArrayBlockingQueue初始化的時候,必須指定隊列長度,是有界的阻塞隊列,所以要預(yù)估好隊列長度,保證生產(chǎn)者和消費者速率相匹配。
- ArrayBlockingQueue的方法是線程安全的,使用ReentrantLock在操作前后加鎖來保證線程安全。
LinkedBlockingQueue
- LinkedBlockingQueue底層基于鏈表實現(xiàn),支持從頭部彈出數(shù)據(jù),從尾部添加數(shù)據(jù)。
- LinkedBlockingQueue初始化的時候,如果不指定隊列長度,默認長度是Integer最大值,相當于無界隊列,有內(nèi)存溢出風(fēng)險,建議初始化的時候指定隊列長度。
- LinkedBlockingQueue的方法是線程安全的,分別使用了讀寫兩把鎖,比ArrayBlockingQueue性能更好。
與ArrayBlockingQueue區(qū)別是:
- 底層結(jié)構(gòu)不同,ArrayBlockingQueue底層基于數(shù)組實現(xiàn),初始化的時候必須指定數(shù)組長度,無法擴容。LinkedBlockingQueue底層基于鏈表實現(xiàn),鏈表最大長度是Integer最大值。
- 占用內(nèi)存大小不同,ArrayBlockingQueue一旦初始化,數(shù)組長度就確定了,不會隨著元素增加而改變。LinkedBlockingQueue會隨著元素越多,鏈表越長,占用內(nèi)存越大。
- 性能不同,ArrayBlockingQueue的入隊和出隊共用一把鎖,并發(fā)較低。LinkedBlockingQueue入隊和出隊使用兩把獨立的鎖,并發(fā)情況下性能更高。
- 公平鎖選項,ArrayBlockingQueue初始化的時候,可以指定使用公平鎖或者非公平鎖,公平鎖模式下,可以按照線程等待的順序來操作隊列。LinkedBlockingQueue只支持非公平鎖。
- 適用場景不同,ArrayBlockingQueue適用于明確限制隊列大小的場景,防止生產(chǎn)速度大于消費速度的時候,造成內(nèi)存溢出、資源耗盡。LinkedBlockingQueue適用于業(yè)務(wù)高峰期可以自動擴展消費速度的場景。
SynchronousQueue
無論是ArrayBlockingQueue還是LinkedBlockingQueue都是起到緩沖隊列的作用,當消費者的消費速度跟不上時,任務(wù)就在隊列中堆積,需要等待消費者慢慢消費。
如果我們想要自己的任務(wù)快速執(zhí)行,不要積壓在隊列中,該怎么辦?這時候就可以使用SynchronousQueue了。
SynchronousQueue被稱為同步隊列,當生產(chǎn)者往隊列中放元素的時候,必須等待消費者把這個元素取走,否則一直阻塞。消費者取元素的時候,同理也必須等待生產(chǎn)者放隊列中放元素。
- SynchronousQueue底層有兩種實現(xiàn)方式,分別是基于棧實現(xiàn)非公平策略,以及基于隊列實現(xiàn)的公平策略。
- SynchronousQueue初始化的時候,可以指定使用公平策略還是非公平策略。
- SynchronousQueue不存儲元素,不適合作為緩存隊列使用。適用于生產(chǎn)者與消費者速度相匹配的場景,可減少任務(wù)執(zhí)行的等待時間。
PriorityBlockingQueue
由于PriorityQueue跟前幾個阻塞隊列不一樣,并沒有實現(xiàn)BlockingQueue接口,只是實現(xiàn)了Queue接口,所以PriorityQueue并不算阻塞隊列。Queue接口中定義了幾組放數(shù)據(jù)和取數(shù)據(jù)的方法,來滿足不同的場景。
- PriorityQueue實現(xiàn)了Queue接口,提供了兩組放數(shù)據(jù)和讀數(shù)據(jù)的方法,來滿足不同的場景。
- PriorityQueue底層基于數(shù)組實現(xiàn),實現(xiàn)了按照元素值大小排序的功能,內(nèi)部按照最小堆存儲,實現(xiàn)了高效的插入和刪除。
- PriorityQueue初始化的時候,可以指定數(shù)組長度和自定義比較器。
- PriorityQueue初始容量是11,當數(shù)組容量小于64,采用2倍擴容,否則采用1.5擴容。
- PriorityQueue每次都是從數(shù)組頭節(jié)點取元素,取之后需要調(diào)整最小堆。
DelayQueue
DelayQueue是一種本地延遲隊列,比如希望我們的任務(wù)在5秒后執(zhí)行,就可以使用DelayQueue實現(xiàn)。常見的使用場景有:
- 訂單10分鐘內(nèi)未支付,就取消。
- 緩存過期后,就刪除。
- 消息的延遲發(fā)送等。
- DelayQueue底層采用組合的方式,復(fù)用PriorityQueue的按照延遲時間排序任務(wù)的功能,實現(xiàn)了延遲隊列。
- DelayQueue是線程安全的,內(nèi)部使用ReentrantLock加鎖。
總結(jié)
這5種阻塞隊列的特性各不相同,在使用的時候該怎么選擇呢?我做了一張圖,供大家參考。