「原創」Java並發編程系列24|信號量Semaphore

2020-12-12 酷扯兒

本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫

本篇介紹第三個並發工具類Semaphore,Semaphore可以理解為信號量,用於控制資源能夠被並發訪問的線程數量,以保證多個線程能夠合理的使用特定資源。分以下部分介紹:

Semaphore的使用Semaphore源碼解析1. Semaphore的使用

Semaphore管理著一組許可permit,許可的初始數量通過構造函數設定。

當線程要訪問共享資源時,需要先通過acquire()方法獲取許可。獲取到之後許可就被當前線程佔用了,在歸還許可之前其他線程不能獲取這個許可。

調用acquire()方法時,如果沒有許可可用了,就將線程阻塞,等待有許可被歸還了再執行。

當執行完業務功能後,需要通過release()方法將許可證歸還,以便其他線程能夠獲得許可證繼續執行。

如果初始化了一個許可為1的Semaphore,那麼就相當於一個不可重入的互斥鎖(Mutex)。

舉個例子理解一下:

我們假設停車場僅有3個停車位,停車位就是有限的共享資源,許可數為3。一開始停車場沒有車輛所有車位全部空著,然後先後到來三輛車,停車場車位夠,安排進去停車。之後來的車必須在外面候著,直到停車場有空車位。當停車場有車開出去,裡面有空位了,則安排一輛車進去(至於是哪輛要看選擇的機制是公平還是非公平)。

從程序角度看,停車場就相當於有限的公共資源,許可數為3,車輛就相當於線程。當來一輛車時,許可數就會減1,當停車場沒有車位了(許可數為0),其他來的車輛需要在外面等候著。如果有一輛車開出停車場,許可數+1,然後放進來一輛車。

代碼實現如下:

public class SemaphoreTest {public static void main(String[] args) { Parking parking = new Parking(3);// 只能停3輛車的停車場 for (int i = 0; i < 8; i++) { // 每一個線程表示一輛車到停車場停車 new Thread() { public void run() { parking.park();// 進入停車場 }; }.start(); } } static class Parking { private Semaphore semaphore;// 信號量 Parking(int count) { semaphore = new Semaphore(count); } public void park() { try { semaphore.acquire();// 獲取許可 long time = (long) (Math.random() * 10); System.out.println(Thread.currentThread().getName() + "進入停車場,停車" + time + "秒..."); Thread.sleep(time);// 獲取許可 System.out.println(Thread.currentThread().getName() + "開出停車場..."); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } } }}

輸出結果:

Thread-2進入停車場,停車5秒...Thread-1進入停車場,停車7秒...Thread-0進入停車場,停車3秒...Thread-0開出停車場...Thread-3進入停車場,停車6秒...Thread-2開出停車場...Thread-4進入停車場,停車3秒...Thread-1開出停車場...Thread-5進入停車場,停車7秒...Thread-4開出停車場...Thread-6進入停車場,停車8秒...Thread-3開出停車場...Thread-7進入停車場,停車0秒...Thread-7開出停車場...Thread-5開出停車場...Thread-6開出停車場...

Semaphore可以用於做流量控制,特別是公共資源有限的應用場景,比如資料庫連接。假如有多個線程讀取數據後,需要將數據保存在資料庫中,而可用的最大資料庫連接只有10個,這時候就需要使用Semaphore來控制能夠並發訪問到資料庫連接資源的線程個數最多只有10個。在限制資源使用的應用場景下,Semaphore是特別合適的。

2. 源碼分析

2.1 類結構

Semaphore同樣是由AQS實現的,用內部類Sync來管理鎖,Sync有兩個實現,分別為NonfairSync(非公平鎖)和FairSync(公平鎖)。

這個類結構有沒有似曾相識的感覺,重入鎖ReentrantLock也是同樣的類結構,Semaphore的源碼跟ReentrantLock有很多相似但又比ReentrantLock簡單。

public class Semaphore implements java.io.Serializable {private final Sync sync; abstract static class Sync extends AbstractQueuedSynchronizer {} static final class NonfairSync extends Sync {} static final class FairSync extends Sync {}}

看下構造方法,設置許可數permits其實就是將AQS.state設置為permits。

public Semaphore(int permits) {sync = new NonfairSync(permits);}NonfairSync(int permits) { super(permits);}Sync(int permits) { setState(permits);}

2.2 acquire()

acquire()方法就是獲取許可,獲取到許可就可以繼續執行訪問共享資源,獲取不到就阻塞等待其他線程歸還許可。

AQS.state用來記錄可用的許可數量,每獲取一個許可state減1。

/*** 獲取許可的方法其實就是獲取鎖的方法 */public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1);}public final void acquireSharedInterruptibly(int arg) throws InterruptedException { // 響應打斷 if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) // 真正獲取鎖的方法,由Semaphore.NonfairSync實現 doAcquireSharedInterruptibly(arg); // 獲取鎖失敗,當前線程阻塞並進入AQS同步隊列}/** * Semaphore.NonfairSync實現的獲取鎖的方法 */protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires);}/** * 每獲取一個許可,將state-1,state表示剩餘的許可數 * 如果許可已經用完,返回remaining<0,表示獲取不到鎖/許可,線程阻塞 * 如果還有許可,返回remaining>=0,表示獲取到鎖/許可,線程繼續執行 */final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires;// 每獲取一個許可,將state-1,state表示剩餘的許可數 if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; }}

2.3 release()

release()方法歸還許可,其實就是將AQS.state加1。歸還成功,喚醒AQS隊列中等鎖的線程,從被阻塞的位置開始執行。

/*** 釋放許可調用釋放鎖的方法 */public void release() { sync.releaseShared(1);}/** * 釋放鎖,完全成功,依次喚醒AQS隊列中等待共享鎖的線程 */public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { // 釋放鎖,由Semaphore.Sync實現 doReleaseShared(); // 釋放鎖成功,喚醒AQS隊列中等鎖的線程 return true; } return false;}/** * 每歸還一個許可將state加1 */protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); int next = current + releases;// 每歸還一個許可將state加1 if (next < current) // overflow throw new Error("Maximum permit count exceeded"); if (compareAndSetState(current, next)) return true; }}

3. 總結

信號量Semaphore用於控制資源能夠被並發訪問的線程數量,以保證多個線程能夠合理的使用特定資源,比如資料庫連接等。

Semaphore在構造時設置一個許可數量,這個許可數量用AQS.state來記錄。

acquire()方法就是獲取許可,只有獲取到許可才可以繼續執行訪問共享資源,獲取到許可之後AQS.state減1,以記錄當前可用的許可數量;如果獲取不到許可,線程就阻塞等待其他線程歸還許可。

release()方法將許可歸還,AQS.state加1;歸還之後,喚醒AQS隊列中阻塞的線程獲取許可。

相關焦點

  • 「原創」Java並發編程系列13|LookSupport
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 13 篇,文末有本系列文章匯總。java.util.concurrent 中源碼頻繁使用的 LockSupport 來阻塞線程和喚醒線程,如 AQS 的底層實現用到 LockSupport.park()方法和 LockSupport.unpark()方法。LockSupport 到底是什麼?
  • 原創】Java並發編程系列01|開篇獲獎感言
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫為什麼要學並發編程我曾聽一個從事15年開發工作的技術人員說過,他剛工作時的並發編程第一原則就是不要寫並發程序。
  • 「原創」Java並發編程系列09|基礎乾貨
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第9篇。現在,我們進入正題:介紹並發編程的基礎性概念。
  • 「原創」Java並發編程系列18|讀寫鎖(下)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 18 篇,文末有本系列文章匯總。上篇為【原創】Java並發編程系列17 | 讀寫鎖八講(上),沒看過的可以先看看。本文是下篇,從「源碼分析寫鎖的獲取與釋放」開始。7.
  • 「原創」Java並發編程系列06|你不知道的final
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫final在Java中是一個保留的關鍵字,可以修飾變量、方法和類。那麼fianl在並發編程中有什麼作用呢?本文就在對final常見應用總結基礎上,講解final並發編程中的應用。
  • 「原創」Java並發編程系列14|AQS源碼分析
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 14 篇,文末有本系列文章匯總。AbstractQueuedSynchronizer是Java並發包java.util.concurrent的核心基礎組件,是實現Lock的基礎。
  • 「原創」Java並發編程系列26|ConcurrentHashMap(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫終於輪到ConcurrentHashMap了,並發編程必備,也是面試必備。HashMap 作為使用最頻繁的集合之一,在多線程環境下是不能用的,因為 HashMap 的設計上就沒有考慮並發環境,極易導致線程安全問題。為了解決該問題,提供了 Hashtable 和 Collections.synchronizedMap(hashMap)兩種解決方案,但是這兩種方案都是對讀寫加獨佔鎖,一個線程在讀時其他線程必須等待,吞吐量和性能都較低。
  • 「原創」Java並發編程系列33|深入理解線程池(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫並發編程必不可少的線程池,接下來分兩篇文章介紹線程池,本文是第一篇。介紹1.1 使用場景並發編程可以高效利用CPU資源,提升任務執行效率,但是多線程及線程間的切換也伴隨著資源的消耗。當遇到單個任務處理時間比較短,但需要處理的任務數量很大時,線程會頻繁的創建銷毀,大量的時間和資源都會浪費在線程的創建和銷毀上,效率很低。
  • 「零基礎學JAVA」基礎篇 第二章 JAVA編程初體驗
    JAVA【零基礎學編程】系列今天給大家帶來基礎篇 第二章 JAVA編程初體驗本節的部分編碼操作需要先安裝JDK開發工具「零基礎學JAVA」工具篇 JDK的安裝教程(WINDOWS版)「零基礎學JAVA」工具篇 環境變量的配置(WINDOWS版)
  • Java並發編程系列20|StampedLock源碼解析
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 20 篇,文末有本系列文章匯總。上一篇介紹了StampedLock存在的意義以及如何使用,按照這個系列的風格大家也應該猜到了,這一篇就是的源碼分析。
  • NO.001- 簡說 Java 並發編程史
    這篇文章是Java並發編程思想系列的第一篇,主要從理解Java並發編程歷史的原因和Java並發演進過程兩部分,以極簡地回溯並發編程的歷史,幫助大家從歷史這個角度去了解一門語言一個特性的演進。對歷史理解的越多,思考的越多,未來的方向就會更加堅定。我是誰?從哪來?到哪去?
  • 「原創」Java並發編程系列36|FutureTask
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫線程池源碼中出現了很多Callable、Future、FutureTask等以前沒介紹過的接口,尤其是線程池提交任務時總是把任務封裝成FutureTask,今天就來為大家解惑:
  • JAVA高並發網絡編程之BIO堵塞網絡編程
    上次說了網絡編程都是有作業系統統一的API的,每個語言有對它的實現,這次來一起說說通過java原生的socket編程完成BIO的網絡編程。
  • Alibaba架構師從零開始,一步一步帶你進入並發編程的世界
    簡介並發與線程安全的基本概念構建與組合線程安全類的技術如何利用java.util.concurrent中的並發構建塊類似地,我們使用線程池和信號量來約束資源的使用,但是卻不能知曉那些管轄範圍內的活動可能形成的資源死鎖(resource deadlock)。Java應用程式不能從死鎖中恢復,所以確保你的設計能夠避免死鎖出現的先決條件是非常有價值的。這一章將講述一些引發活躍度失敗的原因,以及避免發生這些失敗的方法。
  • 大數據入門:Java和Scala編程對比
    在學習大數據之初,很多人都會對程式語言的學習有疑問,比如說大數據編程主要用什麼語言,在實際運用當中,大數據主流編程是Java,但是涉及到Spark、Kafka框架,還需要懂Scala。今天的大數據入門分享,我們就來對Java和Scala這兩門語言的編程做個對比。
  • Java並發編程系列23|循環屏障CyclicBarrier
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本篇介紹第二個並發工具類CyclicBarrier,CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier),分以下部分介紹:
  • Java書架來啦!入門到進階必看書籍推薦
    一直以來求Java書籍推薦的同學就很多,所以我們就整理了下Java方面推薦人數最多的幾本書,希望對大家的學習之路有一定的幫助~本書單共包括4個系列,包括Java入門系列、Java進階系列、Java Web系列、重構與設計系列的必讀書籍。這些都是大家高頻推薦書籍,讀完這些書籍,大家就會大概對Java後端有個全面的認識了。
  • Java 編程,哪些書值得推薦?
    試讀一下,感覺作者的思路很符合從零開始學習 Java 編程,強烈推薦。老馬說編程:《Java編程的邏輯》 - 博客文章列表,肺腑之作,很是敬佩。這本書有電子版,但是作為支持,我還是收藏了這一本的紙質版。
  • 「JAVA」萬字長篇詳述字節碼對象與反射機制完成動態編程
    Java 反射在Java的開發環境中,運行java文件需要使用:java xx.java 命令,運行java命令後,便會啟動JVM,將字節碼文件加載到JVM中,然後開始運行;當運行01類加載機制在Java 類的整個「漫長的」生命周期中,類的加載僅僅只是個開始,因為類在加載後還要經歷一系列的處理,才能被JVM接受,併到處運行
  • Java並發編程系列21|Condition-Lock的等待通知
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫我們知道 synchronized 鎖通過 Object 類的 wait()和 notify()方法實現線程間的等待通知機制,而比 synchronized 更靈活 Lock 鎖同樣也有實現等待通知機制的方式