Java併發編程 ConcurrentLinkedQueue 無界非阻塞隊列 以及 寫時複製容器

ConcurrentLinkedQueue

linkedList的併發版本,無界非阻塞隊列,它是一個基於鏈表的無界線程安全隊列。該隊列的元素 遵循先進先出的原則。頭是最先加入的,尾是最近加入的。插入元素是追加到
尾上。提取一個元素是從頭提取。

大家可以看成是 LinkedList 的併發版本,常用方法:
concurrentLinkedQueue.add(“c”);
concurrentLinkedQueue.offer(“d”);//將指定元素插入到此隊列的尾部。 concurrentLinkedQueue.peek();//檢索並不移除此隊列的頭,如果此隊列爲空,則返回null。 concurrentLinkedQueue.poll();//檢索並移除此隊列的頭,如果此隊列爲空, 則返回null。

寫時複製容器

什麼是寫時複製容器

CopyOnWriteArrayList 和 CopyOnWriteArraySet

CopyOnWrite 容器即寫時複製的容器。通俗的理解是當我們往一個容器添加 元素的時候,不直接往當前容器添加,而是先將當前容器進行
Copy,複製出一 個新的容器,然後新的容器裏添加元素,添加完元素之後,再將原容器的引用指 向新的容器。

這樣做的好處是我們可以對 CopyOnWrite 容器進行併發的讀,而不需要加鎖, 因爲當前容器不會添加任何元素。所以
CopyOnWrite 容器也是一種讀寫分離的思 想,讀和寫不同的容器。如果讀的時候有多個線程正在向
CopyOnWriteArrayList 添加數據,讀還是會讀到舊的數據,因爲寫的時候不會鎖住舊的
CopyOnWriteArrayList。

CopyOnWrite 併發容器用於對於絕大部分訪問都是讀,且只是偶爾寫的併發
場景。比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜索
網站,用戶在這個網站的搜索框中,輸入關鍵字搜索內容,但是某些關鍵字不允
許被搜索。這些不能被搜索的關鍵字會被放在一個黑名單當中,黑名單每天晚上
更新一次。當用戶搜索時,會檢查當前關鍵字在不在黑名單當中,如果在,則提 示不能搜索。

使用 CopyOnWriteMap 需要注意兩件事情:

  1. 減少擴容開銷。根據實際需要,初始化 CopyOnWriteMap 的大小, 避免寫時 CopyOnWriteMap 擴容的開銷。
  2. 使用批量添加。因爲每次添加,容器每次都會進行復制,所以減少添加 次數,可以減少容器的複製次數。

寫時複製容器的問題

性能問題
每次修改都創建一個新數組,然後複製所有內容,如果數組比較大,修改操 作又比較頻繁,可以想象,性能是很低的,而且內存開銷會很大。
數據一致性問題
CopyOnWrite 容器只能保證數據的最終一致性,不能保證數據的實時一致性。 所以如果你希望寫入的的數據,馬上能讀到,不要使用 CopyOnWrite 容器。

阻塞隊列BlockingQueue

隊列
在這裏插入圖片描述

隊列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。進行插入操作的端稱爲隊尾,進行刪除操作的端稱爲隊頭。

在隊列中插入一個隊列元素稱爲入隊,從隊列中刪除一個隊列元素稱爲出隊。 因爲隊列只允許在一端插入,在另一端刪除,所以只有最早進入隊列的元素才能
最先從隊列中刪除,故隊列又稱爲先進先出(FIFO—firstinfirstout)線性表。

什麼是阻塞隊列

1)支持阻塞的插入方法:意思是當隊列滿時,隊列會阻塞插入元素的線程, 直到隊列不滿。
2)支持阻塞的移除方法:意思是在隊列爲空時,獲取元素的線程會等待隊 列變爲非空。

在併發編程中使用生產者和消費者模式能夠解決絕大多數併發問題。該模式 通過平衡生產線程和消費線程的工作能力來提高程序整體處理數據的速度。

在線程世界裏,生產者就是生產數據的線程,消費者就是消費數據的線程。 在多線程開發中,如果生產者處理速度很快,而消費者處理速度很慢,那麼生產 者就必須等待消費者處理完,才能繼續生產數據。同樣的道理,如果消費者的處 理能力大於生產者,那麼消費者就必須等待生產者。

爲了解決這種生產消費能力不均衡的問題,便有了生產者和消費者模式。生 產者和消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。生產者 和消費者彼此之間不直接通信,而是通過阻塞隊列來進行通信,所以生產者生產 完數據之後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據, 而是直接從阻塞隊列裏取,阻塞隊列就相當於一個緩衝區,平衡了生產者和消費 者的處理能力。

阻塞隊列常用於生產者和消費者的場景,生產者是向隊列裏添加元素的線程, 消費者是從隊列裏取元素的線程。阻塞隊列就是生產者用來存放元素、消費者用 來獲取元素的容器。
在這裏插入圖片描述

  • 拋出異常:當隊列滿時,如果再往隊列裏插入元素,會拋出 IllegalStateException(“Queuefull”)異常。當隊列空時,從隊列裏獲取元素會拋 出 NoSuchElementException 異常。
  • 返回特殊值:當往隊列插入元素時,會返回元素是否插入成功,成功返回 true。如果是移除方法,則是從隊列裏取出一個元素,如果沒有則返回 null。
  • 一直阻塞:當阻塞隊列滿時,如果生產者線程往隊列裏 put 元素,隊列會 一直阻塞生產者線程,直到隊列可用或者響應中斷退出。當隊列空時,如果消費 者線程從隊列裏 take 元素,隊列會阻塞住消費者線程,直到隊列不爲空。
  • 超時退出:當阻塞隊列滿時,如果生產者線程往隊列裏插入元素,隊列會 阻塞生產者線程一段時間,如果超過了指定的時間,生產者線程就會退出。

常用阻塞隊列

  • ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列。
  • LinkedBlockingQueue:一個由鏈表結構組成的有界阻塞隊列。
  • PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列。
  • DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
  • SynchronousQueue:一個不存儲元素的阻塞隊列。
  • LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
  • LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。

有界無界 ?

有限隊列就是長度有限,滿了以後生產者會阻塞,無界隊列就是裏面能放 無數的東西而不會因爲隊列長度限制被阻塞,當然空間限制來源於系統資源的限制,如果處理不及時,導致隊列越來越大越來越大,超出一定的限制致使內存超 限,操作系統或者 JVM 幫你解決煩惱,直接把你 OOMkill省事了。

ArrayBlockingQueue

是一個用數組實現的有界阻塞隊列。此隊列按照先進先出(FIFO)的原則對元素進行排序。默認情況下不保證線程公平的訪問隊列,所謂公平訪問隊列是指阻塞的線程,可以按照阻塞的先後順序訪問隊列,即先阻塞線程先訪問隊列。非公平性是對先等待的線程是非公平的,當隊列可用時,阻塞的線程都可以爭奪訪問隊列的資格,有可能先阻塞的線程最後才訪問隊列。初始化時有參數可以設置

LinkedBlockingQueue

是一個用鏈表實現的有界阻塞隊列。此隊列的默認和最大長度爲 Integer.MAX_VALUE。此隊列按照先進先出的原則對元素進行排序。

Array實現 和 Linked實現的區別

  1. 隊列中鎖的實現不同
    ArrayBlockingQueue 實現的隊列中的鎖是沒有分離的,即生產和消費用的是同一個鎖;
    LinkedBlockingQueue 實現的隊列中的鎖是分離的,即生產用的是 putLock, 消費是 takeLock
  2. 在生產或消費時操作不同
    ArrayBlockingQueue 實現的隊列中在生產和消費的時候,是直接將枚舉對象 插入或移除的;
    LinkedBlockingQueue 實現的隊列中在生產和消費的時候,需要把枚舉對象轉 換爲 Node進行插入或移除,會影響性能
  3. 隊列大小初始化方式不同
    ArrayBlockingQueue 實現的隊列中必須指定隊列的大小;
    LinkedBlockingQueue 實現的隊列中可以不指定隊列的大小,但是默認是 Integer.MAX_VALUE

PriorityBlockingQueue

PriorityBlockingQueue 是一個支持優先級的無界阻塞隊列。默認情況下元素 採取自然順序升序排列。也可以自定義類實現compareTo()方法來指定元素排序 規則,或者初始化 PriorityBlockingQueue 時,指定構造參數Comparator 來對元素 進行排序。需要注意的是不能保證同優先級元素的順序。

DelayQueue

是一個支持延時獲取元素的無界阻塞隊列。隊列使用 PriorityQueue 來實現。 隊列中的元素必須實現 Delayed接口,在創建元素時可以指定多久才能從隊列中 獲取當前元素。只有在延遲期滿時才能從隊列中提取元素。

DelayQueue 非常有用,可以將 DelayQueue 運用在以下應用場景。

緩存系統的設計:可以用 DelayQueue 保存緩存元素的有效期,使用一個線 程循環查詢 DelayQueue,一旦能從 DelayQueue 中獲取元素時,表示緩存有效期 到了。還有訂單到期,限時支付等等

SynchronousQueue 是一個不存儲元素

是一個不存儲元素的阻塞隊列。每一個 put 操作必須等待一個 take 操作, 否則不能繼續添加元素。SynchronousQueue可以看成是一個傳球手,負責把生 產者線程處理的數據直接傳遞給消費者線程。隊列本身並不存儲任何元素,非常適合傳遞性場景。SynchronousQueue 的吞吐量高於 LinkedBlockingQueue 和ArrayBlockingQueue。

LinkedTransferQueue

多了 tryTransfer 和 transfer 方法;

(1)transfer 方法

如果當前有消費者正在等待接收元素(消費者使用 take()方法或帶時間限制 的 poll()方法時),transfer方法可以把生產者傳入的元素立刻 transfer(傳輸) 給消費者。如果沒有消費者在等待接收元素,transfer 方法會將元素存放在隊列 的 tail 節點,並等到該元素被消費者消費了才返回。

(2)tryTransfer 方法

tryTransfer 方法是用來試探生產者傳入的元素是否能直接傳給消費者。如果 沒有消費者等待接收元素,則返回 false。和transfer 方法的區別是 tryTransfer 方 法無論消費者是否接收,方法立即返回,而 transfer方法是必須等到消費者消費 了才返回。

LinkedBlockingDeque

LinkedBlockingDeque 是一個由鏈表結構組成的雙向阻塞隊列。所謂雙向隊列指的是可以從隊列的兩端插入和移出元素。雙向隊列因爲多了一個操作隊列的入 口,在多線程同時入隊時,也就減少了一半的競爭。

多了 addFirst、addLast、offerFirst、offerLast、peekFirst 和 peekLast 等方法, 以First 單詞結尾的方法,表示插入、獲取(peek)或移除雙端隊列的第一個元 素。以 Last 單詞結尾的方法,表示插入、獲取或移除雙端隊列的最後一個元素。 另外,插入方法 add 等同於 addLast,移除方法 remove 等效於 removeFirst。但 是take方法卻等同於 takeFirst, 不知道是不是JDK的 bug,使用時還是用帶有First 和 Last 後綴的方法更清楚。在初始化 LinkedBlockingDeque 時可以設置容量防止其過度膨脹。另外,雙向阻塞隊列可以運用在“工作竊取”模式中。

瞭解阻塞隊列的實現原理

使用了等待通知模式實現。所謂通知模式,就是當生產者往滿的隊列裏添加 元素時會阻塞住生產者,當消費者消費了一個隊列中的元素後,會通知生產者當前隊列可用。通過查看 JDK 源碼發現 ArrayBlockingQueue 使用了 Condition 來實 現。其餘隊列的實現,大家可以自行查看,隊列的實現的代碼總體來說,並不複雜。

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