《重學Java高併發》Sempahore的使用場景與常見誤區

中介軟體興趣圈發表於2021-11-28

大家好,我是威哥,《RocketMQ技術內幕》一書作者,榮獲RocketMQ官方社群優秀佈道師、CSDN2020部落格執之星Top2等榮譽稱號。目前擔任中通快遞技術平臺部資深架構師,主要負責全鏈路壓測、訊息中介軟體、資料同步等產品的研發與落地,擁有千億級訊息叢集的運維經驗,不僅實踐經驗豐富,而且對其原始碼有深入且系統的研究。歡迎大家關注我,一起抱團發展。

JUC,java併發框架也是面試中的常客,而Semaphore訊號量又是JUC的重頭戲。

Semaphore簡單嗎?使用起來非常簡單,但最近生產環境遇到一個故障,最終原因竟然是Semaphore在多執行緒非同步環境使用不當造成的?

讀者朋友們一定會覺得很詫異,容我慢慢道來。

文章開頭時先吹一個牛:我有一雙“火眼金睛”,任你刷多少題,是否用過Semaphore,我一問便知。

1、Sempahore使用場景

讀者朋友們對下面的對話我想肯定不會陌生:

面試官:看你簡歷中寫到你熟悉多執行緒程式設計,那你的多執行緒工具包有哪些工具?

候選人:多執行緒jdk提供了豐富的工具,都集中在JUC包中,通常有執行緒池、Semaphore、CountDownLatch、原子類等。
面試官:那你能說說訊號量Semaphore通常在什麼場景下使用呢?
候選人:限流
面試官:如何基於Semaphore進行限流?
候選人:。。。。。。
面試官:看圖說話,這段程式碼是否正確?
在這裡插入圖片描述
Sempahore訊號量最經典的使用場景是限流,用於控制併發度。

回想我們在開發系統時免不了要對接第三方系統,例如在快遞行業的使用者端系統(寄件)時,使用者通過微信小程式進行下單時,需要手動填寫收件人、寄件人資訊等資訊,十分繁瑣與低效,能否從產品角度加以改善?

當然可以,產品提成上傳一張包含收件地址等資訊等圖片,通過AI等技術識別圖片,自動提取圖片中的有效資訊並自動填充,提高使用者體驗。

通過技術選型,我們敲定了百度的免費(付費)圖片識別介面,但第三方提供的配額是有限的,特別是會限制介面的併發度,超過併發度的介面將會返回錯誤,特別是免費類的介面更加如此?

該如何處理呢?Sempahore閃亮登場

2、許可超額現象及解決方案

Sempahore的使用場景非常經典,其使用看上區非常簡單,其基本的使用程式碼如下:
在這裡插入圖片描述
上面的方法就是控制doSomeThing()方法的呼叫併發度,即同一時間允許多少個執行緒併發執行doSomeThing()方法中的程式碼,簡單說明一下:

  • 在建立Sempahore物件時指定該訊號量中包含的許可數量。
  • 在執行業務方法之前,首先向訊號量物件中申請一個許可,如果申請到,acquire()方法立即返回,執行完成業務邏輯放一定要呼叫release()方法,釋放許可。
  • 如果當前許可已全部申請,其他執行緒呼叫訊號量的acquire()方法時會阻塞,當然acquire方法也可以設定超時時間,該方法的返回結果表示是否申請到許可。

溫馨提示:上面的程式碼有漏洞,你能發現嗎?

上面的程式碼有可能造成多釋放,當10個執行緒分別去獲取許可,都能成功,但都在執行doSomeThing()方法的時候,第11個執行緒嘗試獲取許可,但可能發生中斷等異常導致沒成功獲取許可而觸發異常,但最終進入到finally語句塊,進行釋放許可,這樣就會增加許可數量,導致邏輯異常,這樣的問題特別是在呼叫帶超時時間的acquire方法時更加明顯,其正確的使用如下圖所示:
在這裡插入圖片描述

3、許可不釋放導致無限阻塞

上面只是“小試牛刀”,使用Sempahore真正的挑戰在於多執行緒環境中,我們回到開頭部分的場景,呼叫第三方介面解析圖片,並使用多執行緒提高併發度,示例程式碼如下:
在這裡插入圖片描述
該示例主要是結合CompletableFuture實現多執行緒併發,並結合訊號量控制呼叫第三方介面的併發度(這裡用doSomeThing()方法表示)。

溫馨提示:CompleteableFuture的whenComplete事件回撥函式是發生異常時也會進入,並且第二個引數為異常物件。

在JDK8中CompletableFuture暫不支援設定超時時間,故本例使用了CountDownLatch用來控制test02方法的超時時間。

上面的示例是不是非常給力,但如果細看,可能存在許可不及時釋放的問題,也就示說semaphore的release()方法不會執行,因為上述方法會超時。

許可不釋放帶來的後果非常嚴重,因為後續申請的時候由於一直沒有許可,將無法獲取許可,而無法執行業務邏輯。

初步解決思路就是在cdh.awiat方法結束後判斷是否超時,如果超時,手動釋放許可,例如下圖所示:
在這裡插入圖片描述
這樣的想法好是好,但這樣會造成許可的多次釋放,最終導致許可數量增加,超過預期。

這其實是要求seaphore的release方法會在不同條件下在不同地方會被呼叫,但同一個請求只在其中一個地方被執行。

要解決這個問題,我想大家會自然而然的想到JUC中的另外的工具:原子類,儘管多次呼叫,我們只需第一次呼叫時真正釋放許可,其他呼叫則直接忽略即可。

解決方案如下:
在這裡插入圖片描述
引入一個包裝類,包裝Sempahore,並結合AtomicBoolean,保證每一個SempahoreReleaseOnlyOne物件只會釋放Sempahore一次。

引入該類後,上面的程式碼改造如下:
在這裡插入圖片描述
本文就介紹到這裡了,Semaphore看似簡單,但要用好用對還是有難度的,不知各位是否Get到了。如果需要整套原始碼,可以傳送私信:SCODE,即可獲取。

一鍵三連(關注、點贊、留言)是對我最大的鼓勵

掌握一到兩門java主流中介軟體,是敲開BAT等大廠必備的技能,送給大家一個Java中介軟體學習路線,助力大家早日進入網際網路大廠。

Java進階之梯,成長路線與學習資料,助力突破中介軟體領域

最後分享筆者一個硬核的RocketMQ電子書,您將獲得千億級訊息流轉的運維經驗。
在這裡插入圖片描述
獲取方式:RocketMQ電子書

文章首發:https://www.codingw.net/posts/fa8c5e0b.html

相關文章