『并發包入坑指北』之阻塞隊列

006tNc79ly1g1vn9xpgp4j31ak0u013m.jpg

前言

較長一段時間以來我都發現不少開發者對 jdk 中的 J.U.C(java.util.concurrent)也就是 Java 并發包的使用甚少,更別談對它的理解了;但這卻也是我們進階的必備關卡。

之前或多或少也分享過相關內容,但都不成體系;于是便想整理一套與并發包相關的系列文章。

其中的內容主要包含以下幾個部分:

  • 根據定義自己實現一個并發工具。
  • JDK 的標準實現。
  • 實踐案例。

基于這三點我相信大家對這部分內容不至于一問三不知。

既然開了一個新坑,就不想做的太差;所以我打算將這個列表下的大部分類都講到。

006tNc79ly1g1vpwdqbkrj30ab09nmy9.jpg

所以本次重點討論 ArrayBlockingQueue

自己實現

在自己實現之前先搞清楚阻塞隊列的幾個特點:

  • 基本隊列特性:先進先出。
  • 寫入隊列空間不可用時會阻塞。
  • 獲取隊列數據時當隊列為空時將阻塞。

實現隊列的方式多種,總的來說就是數組和鏈表;其實我們只需要搞清楚其中一個即可,不同的特性主要表現為數組和鏈表的區別。

這里的 ArrayBlockingQueue 看名字很明顯是由數組實現。

我們先根據它這三個特性嘗試自己實現試試。

初始化隊列

我這里自定義了一個類:ArrayQueue,它的構造函數如下:

    public ArrayQueue(int size) {items = new Object[size];}

很明顯這里的 items 就是存放數據的數組;在初始化時需要根據大小創建數組。

006tNc79ly1g1wd71n229j30wb0u043w.jpg

寫入隊列

寫入隊列比較簡單,只需要依次把數據存放到這個數組中即可,如下圖:

006tNc79ly1g1we7yeykej30b0060mxc.jpg

但還是有幾個需要注意的點:

  • 隊列滿的時候,寫入的線程需要被阻塞。
  • 寫入過隊列的數量大于隊列大小時需要從第一個下標開始寫。

先看第一個隊列滿的時候,寫入的線程需要被阻塞,先來考慮下如何才能使一個線程被阻塞,看起來的表象線程卡住啥事也做不了。

有幾種方案可以實現這個效果:

  • Thread.sleep(timeout)線程休眠。
  • object.wait() 讓線程進入 waiting 狀態。

當然還有一些 join、LockSupport.part 等不在本次的討論范圍。

阻塞隊列還有一個非常重要的特性是:當隊列空間可用時(取出隊列),寫入線程需要被喚醒讓數據可以寫入進去。

所以很明顯Thread.sleep(timeout)不合適,它在到達超時時間之后便會繼續運行;達不到空間可用時才喚醒繼續運行這個特點。

其實這樣的一個特點很容易讓我們想到 Java 的等待通知機制來實現線程間通信;更多線程見通信的方案可以參考這里:深入理解線程通信

所以我這里的做法是,一旦隊列滿時就將寫入線程調用 object.wait() 進入 waiting 狀態,直到空間可用時再進行喚醒。

    /*** 隊列滿時的阻塞鎖*/private Object full = new Object();/*** 隊列空時的阻塞鎖*/private Object empty = new Object();

006tNc79ly1g1wf8de8jzj30te0tin1i.jpg

所以這里聲明了兩個對象用于隊列滿、空情況下的互相通知作用。

在寫入數據成功后需要使用 empty.notify(),這樣的目的是當獲取隊列為空時,一旦寫入數據成功就可以把消費隊列的線程喚醒。

這里的 wait 和 notify 操作都需要對各自的對象使用 synchronized 方法塊,這是因為 wait 和 notify 都需要獲取到各自的鎖。

消費隊列

上文也提到了:當隊列為空時,獲取隊列的線程需要被阻塞,直到隊列中有數據時才被喚醒。

006tNc79ly1g1wfhr3r6qj30tg0tiwit.jpg

代碼和寫入的非常類似,也很好理解;只是這里的等待、喚醒恰好是相反的,通過下面這張圖可以很好理解:

006tNc79ly1g1wfwr016gj30o20ksq59.jpg

總的來說就是:

  • 寫入隊列滿時會阻塞直到獲取線程消費了隊列數據后喚醒寫入線程
  • 消費隊列空時會阻塞直到寫入線程寫入了隊列數據后喚醒消費線程

測試

先來一個基本的測試:單線程的寫入和消費。

006tNc79ly1g1wg97uqgpj30uu0dqwgu.jpg

3
123
1234
12345

通過結果來看沒什么問題。


當寫入的數據超過隊列的大小時,就只能消費之后才能接著寫入。

006tNc79ly1g1wgmshqfyj316o0n2ae5.jpg

2019-04-09 16:24:41.040 [Thread-0] INFO  c.c.concurrent.ArrayQueueTest - [Thread-0]123
2019-04-09 16:24:41.040 [main] INFO  c.c.concurrent.ArrayQueueTest - size=3
2019-04-09 16:24:41.047 [main] INFO  c.c.concurrent.ArrayQueueTest - 1234
2019-04-09 16:24:41.048 [main] INFO  c.c.concurrent.ArrayQueueTest - 12345
2019-04-09 16:24:41.048 [main] INFO  c.c.concurrent.ArrayQueueTest - 123456

從運行結果也能看出只有當消費數據后才能接著往隊列里寫入數據。


006tNc79ly1g1wiskvki8j30yy0eo0ve.jpg

006tNc79ly1g1witm4twpj31q60ai0vz.jpg

而當沒有消費時,再往隊列里寫數據則會導致寫入線程被阻塞。

并發測試

006tNc79ly1g1wiwyz4j5j30vz0u044f.jpg

三個線程并發寫入300條數據,其中一個線程消費一條。

=====0
299

最終的隊列大小為 299,可見線程也是安全的。

由于不管是寫入還是獲取方法里的操作都需要獲取鎖才能操作,所以整個隊列是線程安全的。

ArrayBlockingQueue

下面來看看 JDK 標準的 ArrayBlockingQueue 的實現,有了上面的基礎會更好理解。

初始化隊列

006tNc79ly1g1wkaau8w7j30ze0lcagb.jpg

看似要復雜些,但其實逐步拆分后也很好理解:

第一步其實和我們自己寫的一樣,初始化一個隊列大小的數組。

第二步初始化了一個重入鎖,這里其實就和我們之前使用的 synchronized 作用一致的;

只是這里在初始化重入鎖的時候默認是非公平鎖,當然也可以指定為 true 使用公平鎖;這樣就會按照隊列的順序進行寫入和消費。

更多關于 ReentrantLock 的使用和原理請參考這里:ReentrantLock 實現原理

三四兩步則是創建了 notEmpty notFull 這兩個條件,他的作用于用法和之前使用的 object.wait/notify 類似。

這就是整個初始化的內容,其實和我們自己實現的非常類似。

寫入隊列

006tNc79ly1g1wktuhxzuj30tk0bqq55.jpg
006tNc79ly1g1wktfkwu2j30ug09ugnn.jpg

其實會發現阻塞寫入的原理都是差不多的,只是這里使用的是 Lock 來顯式獲取和釋放鎖。

同時其中的 notFull.await();notEmpty.signal(); 和我們之前使用的 object.wait/notify 的用法和作用也是一樣的。

當然它還是實現了超時阻塞的 API

006tNc79ly1g1wl1n7ir5j30vm0iqdjb.jpg

也是比較簡單,使用了一個具有超時時間的等待方法。

消費隊列

再看消費隊列:

006tNc79ly1g1wl3vcsioj30tc0ayq4y.jpg
006tNc79ly1g1wl4cfrnlj30u00eq0vm.jpg

也是差不多的,一看就懂。

而其中的超時 API 也是使用了 notEmpty.awaitNanos(nanos) 來實現超時返回的,就不具體說了。

實際案例

說了這么多,來看一個隊列的實際案例吧。

背景是這樣的:

有一個定時任務會按照一定的間隔時間從數據庫中讀取一批數據,需要對這些數據做校驗同時調用一個遠程接口。

簡單的做法就是由這個定時任務的線程去完成讀取數據、消息校驗、調用接口等整個全流程;但這樣會有一個問題:

假設調用外部接口出現了異常、網絡不穩導致耗時增加就會造成整個任務的效率降低,因為他都是串行會互相影響。

所以我們改進了方案:

006tNc79ly1g1wm1v7mfxj30qs0aiq4g.jpg

其實就是一個典型的生產者消費者模型:

  • 生產線程從數據庫中讀取消息丟到隊列里。
  • 消費線程從隊列里獲取數據做業務邏輯。

這樣兩個線程就可以通過這個隊列來進行解耦,互相不影響,同時這個隊列也能起到緩沖的作用。

但在使用過程中也有一些小細節值得注意。

因為這個外部接口是支持批量執行的,所以在消費線程取出數據后會在內存中做一個累加,一旦達到閾值或者是累計了一個時間段便將這批累計的數據處理掉。

但由于開發者的大意,在消費的時候使用的是 queue.take() 這個阻塞的 API;正常運行沒啥問題。

可一旦原始的數據源,也就是 DB 中沒數據了,導致隊列里的數據也被消費完后這個消費線程便會被阻塞。

這樣上一輪積累在內存中的數據便一直沒機會使用,直到數據源又有數據了,一旦中間間隔較長時便可能會導致嚴重的業務異常。

所以我們最好是使用 queue.poll(timeout) 這樣帶超時時間的 api,除非業務上有明確的要求需要阻塞。

這個習慣同樣適用于其他場景,比如調用 http、rpc 接口等都需要設置合理的超時時間。

總結

關于 ArrayBlockingQueue 的相關分享便到此結束,接著會繼續更新其他并發容器及并發工具。

對本文有任何相關問題都可以留言討論。

本文涉及到的所有源碼:

https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/concurrent/ArrayQueue.java

你的點贊與分享是對我最大的支持

轉載于:https://www.cnblogs.com/crossoverJie/p/10681080.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/449267.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/449267.shtml
英文地址,請注明出處:http://en.pswp.cn/news/449267.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

個人理財有哪些基本原理和方法?

現金為王:不超額消費,不使用信用卡,不負債(房貸除外) 信貸消費已經成為主流的今天,強調使用現金似乎與時代格格不入。而對于信貸消費的依賴,常常來自于下面幾個看起來十分有力的觀點&#xff…

2019年3月4日 701. Insert into a Binary Search Tree

比較基礎的二叉樹排序樹插入,寫了個遞歸。# Definition for a binary tree node. # class TreeNode(object): # def __init__(self, x): # self.val x # self.left None # self.right Noneclass Solution(object):def insertIntoBST…

2020-3-18

題目一: JavaScript 字符串轉換為數組 其一: let str"apple"; console.log([...str]);運行結果 其二(使用split()): let str"apple"; console.log(str.split());注1:如果將參數省略…

思維導圖,流程圖模板整合

思維導圖與流程圖在工作中都是經常使用的,出現頻率較高的,有些不會繪制的或者是剛接觸這一類的圖表形式的都會選擇使用模板來完成工作,但是很多朋友卻不知道模板在,今天要給大家分享的是幾款孩子走精美的思維導圖,流程…

解決 List 執行 remove 時報異常 java.lang.UnsupportedOperationException

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 一、情況描述 報錯如題: java.lang.UnsupportedOperationException: nullat java.util.Collections$UnmodifiableCollectio…

2020-3-19

題目一&#xff1a; js split() 分割字符串生成數組 let str"I am a student"; let arrstr.split(" "); for(let i0;i<arr.length;i){console.log(arr[i]); }分析&#xff1a;這里利用字符串的空格來分割字符串生成數組。split()方法的參數設置為"…

上班族怎么創業?白領一族創業當老板!

班族怎么創業?很多上班族無法面對每天平淡的生活&#xff0c;于是想要擁有一份屬于自己的事業。上班族創業有哪些好的項目呢?結合自已的興趣愛好&#xff0c;找到適合的項目&#xff0c;上班的同時也能當老板。 上班族怎么創業?創業項目1、開投資額小的特色店 嘗試開店創業的…

一文告訴你 Event Loop 是什么?

Event Loop 也叫做“事件循環”&#xff0c;它其實與 JavaScript 的運行機制有關。 JS初始設計 JavaScript 在設計之初便是單線程&#xff0c;程序運行時&#xff0c;只有一個線程存在&#xff0c;在特定的時候只能有特定的代碼被執行。這和 JavaScript 的用途有關&#xff0c;…

Spring Boot -Shiro配置多Realm

2019獨角獸企業重金招聘Python工程師標準>>> 核心類簡介 xxxToken&#xff1a;用戶憑證 xxxFilter&#xff1a;生產token&#xff0c;設置登錄成功&#xff0c;登錄失敗處理方法&#xff0c;判斷是否登錄連接等 xxxRealm&#xff1a;依據配置的支持Token來認證用戶信…

idea工具debug斷點紅色變成灰色

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 沒事別瞎點&#xff0c;禁用了斷點當然不走了 轉自&#xff1a;https://blog.csdn.net/anlve512/article/details/54583469

2020-3-20前端題目

題目一&#xff1a; 判斷checked復選框是否有被選中 <!DOCTYPE html> <html> <head> <meta charset" utf-8"> <script> window.onload () > {let odivdocument.getElementById("ant");let ckdocument.getElementById(&…

上班族如何當老板 五大模式任你選

中國教育在線訊 辭職創業&#xff0c;還是維持現在穩定的工作?這個是很多上班族都糾結過的問題&#xff0c;一邊是穩定的工作和收入&#xff0c;一邊是創業當老板的誘惑&#xff0c;真是很難選擇。 其實&#xff0c;如果安排合理是可以“魚與熊掌”兼得的&#xff0c;沈陽市古…

利用 Linux tap/tun 虛擬設備寫一個 ICMP echo 程序

利用 Linux tap/tun 虛擬設備寫一個 ICMP echo 程序 前面兩篇文章已經介紹過 tap/tun 的原理和配置工具。這篇文章通過一個編程示例來深入了解 tap/tun 的程序結構。 01 準備工作 首先通過 modinfo tun 查看系統內核是否支持 tap/tun 設備驅動。 Copy[rootby ~]# modinfo tun f…

2020-3-21

題目一&#xff1a; JavaScript 獲取月份最后一天日期 月份最后一天日期可能是不同的&#xff0c;比如有的是30、有的是31還有的是28。 <!DOCTYPE html><html> <head> <meta charset" utf-8"> <script type"text/javascript"&…

正方形矩陣求對角線之和

nint(input()) a[] for i in range(n): #循環體里面加入input&#xff08;&#xff09;可以實現一共執行n次input&#xff08;&#xff09; lst[int(x) for x in input().split()]a.append(lst) #用列表解析&#xff0c;兩層列表代表行列&#xff0c;很巧妙的方法 w0 bl…

解決: Unable to connect to zookeeper server within timeout: 5000

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 一個項目啟動不起來了&#xff0c;報錯如題&#xff1a; Caused by: org.I0Itec.zkclient.exception.ZkTimeoutException: Unable to c…

閑錢請看如何處理

買一點基金定投。基金是專家幫你理財。基金的起始資金最低單筆是1000元,定投200元起投 買基金到銀行或者基金公司都行。銀行能代理很多基金公司的業務&#xff0c;具體開戶找銀行理財專柜辦理。現在有些證券公司也有代理基金買賣的。在銀行開通網上銀行后網上購買一般收費上有優…

JAVA 數組元素的反轉

package Code411;/*數組元素的反轉本來[1,2,3,4]反轉后[4,3,2,1]1.對稱位置的元素交換2.對稱位子需要兩個索引3.int temp a&#xff1b;ab;btemp;4.什么時候停止交換&#xff08;1&#xff09;minmax (2)min>max */public class CodeArrayReverse { public static void m…

requests模塊相關用法

requests模塊 -1. 什么是requests模塊- python原生的一個基于網絡請求的模塊&#xff0c;模擬瀏覽器發起請求。 -2. 為什么使用requests模塊-1. 自動處理url編碼-2. 自動處理post請求參數-3. 簡化cookie和代理的操作-3. requests模塊如何被使用安裝&#xff1a; pip install re…

2020-3-22

題目一&#xff1a; JavaScript 天小時分鐘和秒倒計時 代碼與解析&#xff1a; <!DOCTYPE html> <html> <head> <meta charset" utf-8"> <style type"text/css"> *{margin:0;padding:0;list-style:none; } body{font-size:…