一文帶你掌握Java?LinkedBlockingQueue
開篇語
隊列在生活中隨處可見,醫(yī)院繳費需要排隊、做核酸需要排隊、汽車等紅綠燈需要排隊等等。
隊列是一個按照先來到就排在前面,后來到排在后面的數(shù)據(jù)結(jié)構(gòu),并且出隊的時候也是按照先來到先出隊。使用數(shù)組和鏈表進行實現(xiàn)。通常用于協(xié)調(diào)任務(wù)的執(zhí)行和數(shù)據(jù)的交換。
介紹
LinkedBlockingQueue 是一個可選有界阻塞隊列,有界指的是隊列存在一個最大容量;阻塞指的是如果隊列已經(jīng)滿了,想要往隊列繼續(xù)添加元素的話,那么這個操作將會被暫停,直到隊列中有空位才會繼續(xù)完成添加操作。如果隊列已經(jīng)為空,想要從隊列中獲取元素,那么這個操作將會被暫停,直接隊列中存在元素才會繼續(xù)完成獲取操作。
實現(xiàn)原理
LinkedBlockingQueue 內(nèi)部使用鏈表作為元素的存儲結(jié)構(gòu)。內(nèi)部使用了兩個鎖,分別使用于存操作和取操作。
執(zhí)行存取操作時,都必須先獲取鎖,才可以執(zhí)行存取操作,保證 LinkedBlockingQueue 是線程安全。
LinkedBlockingQueue 通過兩個 Condition 條件隊列,一個 notFull 條件,一個 notEmpty 條件。在對隊列進行插入元素操作時,判斷當前隊列已經(jīng)滿,則通過 notFull 條件將線程阻塞,直到其他線程通知該線程隊列可以繼續(xù)插入元素。在對隊列進行移除元素操作時,判斷當前隊列已經(jīng)空,則通過 notEmpty 條件阻塞線程,直到其他線程通過該線程可以繼續(xù)獲取元素。
這樣保證線程的存取操作不會出現(xiàn)錯誤。避免隊列在滿時,丟棄插入的元素;也避免在隊列空時取到一個 null 值。
構(gòu)造函數(shù)
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); } public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); }
無參構(gòu)造函數(shù)中,默認使用 Integer.MAX_VALUE
作為隊列的最大容量。
有參構(gòu)造函數(shù)中,可以自己指定隊列的最大容量,并且創(chuàng)建了頭節(jié)點和尾節(jié)點。那么 LinkedBlockingQueue 使用的是有頭單向鏈表。
private final int capacity; /** Current number of elements */ private final AtomicInteger count = new AtomicInteger(); transient Node<E> head; private transient Node<E> last; // 取鎖 private final ReentrantLock takeLock = new ReentrantLock(); private final Condition notEmpty = takeLock.newCondition(); // 存鎖 private final ReentrantLock putLock = new ReentrantLock(); private final Condition notFull = putLock.newCondition();
并且在對象初始化時,創(chuàng)建了兩個鎖,分別使用于存操作和取操作。創(chuàng)建了兩個條件隊列,分別用于隊列空和滿的情況。
插入函數(shù)
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); final int c; final Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { while (count.get() == capacity) { notFull.await(); } enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); }
1.獲取鎖
2.判斷當前隊列是否已經(jīng)滿了
- 如果隊列已經(jīng)滿了,調(diào)用 notFull 條件隊列的 await() 方法,將該線程阻塞,暫停該線程的插入操作。避免內(nèi)部溢出的問題。
- 如果沒有滿,則直接調(diào)用入隊函數(shù) enqueue 插入到隊列末尾。
3.檢查此時隊列是否已滿
如果未滿,則調(diào)用 notFull 條件隊列的 signal() 方法,喚醒被阻塞在 notFull 條件隊列的線程。
4.解鎖
- 檢查插入元素前的隊列元素數(shù)量是否等于0
- 等于 0,則調(diào)用 notEmpty 條件隊列的 signal() 方法,通知其隊列現(xiàn)在不為空,可以喚醒阻塞線程獲取元素。
為什么需要調(diào)用 notFull 條件隊列的 signal() 方法? 因為隊列取操作和存操作所使用的鎖是不一樣的,那么就說明,一個線程執(zhí)行存入操作時,其他線程是可以執(zhí)行取出操作的。我們來看下面這個例子:
- 隊列總?cè)萘繛?5,當前元素數(shù)量為5。線程 A 獲取了存鎖,想要插入了元素。但是因為隊列容量已滿,釋放鎖,并且加入到條件隊列中,等待被喚醒。
- 線程 B 獲取了存鎖,想要插入了元素。但是因為隊列容量已滿,釋放鎖,并且加入到條件隊列中,等待被喚醒。
- 線程 C 獲取了取鎖,取出了元素 1。并且通過 notFull 的 signal 方法喚醒條件隊列中被阻塞的線程 A。線程 A 被喚醒后加入到同步隊列中,但是此時還沒有競爭到鎖。
- 線程 D 獲取了取鎖,取出了元素 2。但是還沒有執(zhí)行到喚醒阻塞線程的代碼。
- 線程 A 競爭到鎖,開始執(zhí)行插入元素操作。將元素插入之后,檢查到隊列元素數(shù)量為 4,小于隊列的總?cè)萘?,因此會?zhí)行 notFull 的 signal 方法喚醒條件隊列中被阻塞的線程 B。線程 B 被喚醒后加入到同步隊列中,開始競爭鎖。
- 線程 B 競爭到鎖,開始執(zhí)行插入元素操作。將元素插入之后,檢查到隊列元素數(shù)量等于 5,不進行喚醒操作。
這樣做的目的是盡快的喚醒阻塞線程,可以更快的完成插入元素操作。因為線程存和取的操作相互之間并不是互斥的,而是獨立運行的,提高吞吐量。
獲取函數(shù)
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; }
1.獲得取鎖
2.判斷當前隊列是否為空
- 如果隊列沒有元素,調(diào)用 notEmpty 條件隊列的 await() 方法,將該線程阻塞,暫停該線程的獲取操作。避免獲取元素出錯。
- 如果不為空,則直接調(diào)用出隊函數(shù) dequeue 移除隊列第一個元素,并返回給客戶端。
3.檢查此時隊列是否為空
如果不為空,則調(diào)用 notEmpty 條件隊列的 signal() 方法,喚醒被阻塞在 notEmpty 條件隊列的線程。
4.釋放鎖
5.檢查獲取元素前的隊列元素數(shù)量是否等于最大容量
等于最大容量,因為此時已經(jīng)取出一個元素,因此隊列處于未滿的狀態(tài),可以喚醒阻塞在 notFull 條件的線程,讓線程繼續(xù)插入元素。
步驟 3 的目的是盡快的喚醒阻塞線程,可以更快的完成取元素操作。提高吞吐量??梢試L試自己畫出流程圖。
入隊函數(shù)
private void enqueue(Node<E> node) { last = last.next = node; }
入隊函數(shù)極其簡單,只要將最后一個元素的 next 指針指向當前元素即完成了插入操作。
出隊函數(shù)
private E dequeue() { // assert takeLock.isHeldByCurrentThread(); // assert head.item == null; Node<E> h = head; Node<E> first = h.next; h.next = h; // help GC head = first; E x = first.item; first.item = null; return x; }
我們前面說 LinkedBlockingQueue 使用的是有頭鏈表。頭節(jié)點只是作為一個標志,實際上并不是一個真正的元素。當獲取元素時,將頭節(jié)點的下一個節(jié)點作為頭節(jié)點,將原來的頭節(jié)點取消引用,被垃圾回收即可。
應(yīng)用場景
適用場景
LinkedBlockingQueue 和 ArrayBlockingQueue 一樣適用于多個線程之間需要共享數(shù)據(jù)、協(xié)調(diào)任務(wù)執(zhí)行的場景。因此可以總結(jié)出以下幾個應(yīng)用場景:
線程池:線程池是一個常見的并發(fā)編程模型,它通過線程池中的線程執(zhí)行任務(wù)。并且可以重復(fù)使用這些線程。在線程池中,可以使用 LinkedBlockingQueue 來存儲需要執(zhí)行的任務(wù),以此控制任務(wù)數(shù)量和執(zhí)行順序。當線程池中的線程執(zhí)行完任務(wù)之后,可以從 LinkedBlockingQueue 中取出下一個任務(wù)執(zhí)行。
生產(chǎn)者-消費者:在生產(chǎn)者-消費者模型中,生產(chǎn)者負責生產(chǎn)數(shù)據(jù),消費者負責對數(shù)據(jù)進行處理。在這種模式下,LinkedBlockingQueue 可以作為生產(chǎn)者與消費者之間的數(shù)據(jù)通道,保證線程安全和數(shù)據(jù)正確。
實際應(yīng)用場景
- Nacos: Nacos 是一個動態(tài)服務(wù)發(fā)現(xiàn)、配置和服務(wù)管理平臺,它使用 LinkedBlockingQueue 來實現(xiàn)內(nèi)部的任務(wù)隊列。
- Tomcat:從 Tomcat 7 開始,請求隊列默認使用了 LinkedBlockingQueue 實現(xiàn)。
- Hystrix: 一個流行的容錯框架,其默認使用 LinkedBlockingQueue 作為請求隊列。
總結(jié)
LinkedBlockingQueue 和 ArrayBlockQueue 的對比:
- 實現(xiàn)方式不同:LinkedBlockingQueue 是基于鏈表實現(xiàn)的隊列,而 ArrayBlockingQueue 是基于數(shù)組實現(xiàn)的隊列。
- 插入和刪除元素的效率不同:LinkedBlockingQueue 的插入和刪除元素的效率較高,因為它采用了兩把鎖來控制讀寫操作;而 ArrayBlockingQueue 的插入和刪除元素的效率相對較低,因為它采用了一把鎖來控制讀寫操作。
- 內(nèi)存占用不同:LinkedBlockingQueue 的內(nèi)存占用比 ArrayBlockingQueue 更高,因為鏈表結(jié)構(gòu)需要額外的指針存儲,而數(shù)組結(jié)構(gòu)則不需要。
- 阻塞模式不同:ArrayBlockingQueue支持公平模式和非公平模式,可以控制入隊和出隊的順序;而 LinkedBlockingQueue 只支持非公平模式。
到此這篇關(guān)于一文帶你掌握Java LinkedBlockingQueue的文章就介紹到這了,更多相關(guān)Java LinkedBlockingQueue內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java?EasyExcel實現(xiàn)合并相同內(nèi)容單元格與動態(tài)標題功能
這篇文章主要為大家詳細介紹了Java?EasyExcel如何實現(xiàn)合并相同內(nèi)容單元格與動態(tài)標題功能,文中的示例代碼講解詳細,有需要的小伙伴可以參考下2023-12-12詳解Guava Cache本地緩存在Spring Boot應(yīng)用中的實踐
Guava Cache是一個全內(nèi)存的本地緩存實現(xiàn),本文將講述如何將 Guava Cache緩存應(yīng)用到 Spring Boot應(yīng)用中。具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01