并發(fā)編程之 Semaphore 源碼分析

前言

并發(fā) JUC 包提供了很多工具類阎肝,比如之前說的 CountDownLatch香罐,CyclicBarrier ,今天說說這個 Semaphore——信號量逸绎,關(guān)于他的使用請查看往期文章并發(fā)編程之 線程協(xié)作工具類,今天的任務(wù)就是從源碼層面分析一下他的原理夭谤。

源碼分析

如果先不看源碼棺牧,根據(jù)以往我們看過的 CountDownLatch CyclicBarrier 的源碼經(jīng)驗來看,Semaphore 會怎么設(shè)計呢朗儒?

首先颊乘,他要實現(xiàn)多個線程線程同時訪問一個資源,類似于共享鎖醉锄,并且乏悄,要控制進入資源的線程的數(shù)量。

如果根據(jù) JDK 現(xiàn)有的資源榆鼠,我們是否可以使用 AQS 的 state 變量來控制呢纲爸?類似 CountDownLatch 一樣,有幾個線程我們就為這個 state 變量設(shè)置為幾妆够,當(dāng) state 達到了閾值识啦,其他線程就不能獲取鎖了,就需要等待神妹。當(dāng) Semaphore 調(diào)用 release 方法的時候颓哮,就釋放鎖,將 state 減一鸵荠,并喚醒 AQS 上的線程冕茅。

以上,就是我們的猜想蛹找,那我們看看 JDK 是不是和我們想的一樣姨伤。

首先看看 Semaphore 的 UML 結(jié)構(gòu):

image.png

內(nèi)部有 3 個類,繼承了 AQS庸疾。一個公平鎖乍楚,一個非公平鎖,這點和 ReentrantLock 一摸一樣届慈。

看看他的構(gòu)造器:

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

兩個構(gòu)造器徒溪,兩個參數(shù),一個是許可線程數(shù)量金顿,一個是是否公平鎖臊泌,默認非公平。

而 Semaphore 有 2 個重要的方法揍拆,也是我們經(jīng)常使用的 2 個方法:

semaphore.acquire();
// doSomeing.....
semaphore.release();

acquire 和 release 方法渠概,我們今天重點看這兩個方法的源碼,一窺 Semaphore 的全貌礁凡。

acquire 方法源碼分析

代碼如下:

public void acquire() throws InterruptedException {
    // 嘗試獲取一個鎖
    sync.acquireSharedInterruptibly(1);
}

// 這是抽象類 AQS 的方法
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 如果小于0高氮,就獲取鎖失敗了慧妄。加入到AQS 等待隊列中顷牌。
    // 如果大于0剪芍,就直接執(zhí)行下面的邏輯了。不用進行阻塞等待窟蓝。
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
// 這是抽象父類 Sync 的方法罪裹,默認是非公平的
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}
// 非公平鎖的釋放鎖的方法
final int nonfairTryAcquireShared(int acquires) {
    // 死循環(huán)
    for (;;) {
        // 獲取鎖的狀態(tài)
        int available = getState();
        int remaining = available - acquires;
        // state 變量是否還足夠當(dāng)前獲取的
        // 如果小于 0,獲取鎖就失敗了运挫。
        // 如果大于 0状共,就循環(huán)嘗試使用 CAS 將 state 變量更新成減去輸入?yún)?shù)之后的。
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

這里的釋放就是對 state 變量減一(或者更多)的谁帕。

返回了剩余的 state 大小峡继。

當(dāng)返回值小于 0 的時候,說明獲取鎖失敗了匈挖,那么就需要進入 AQS 的等待隊列了碾牌。代碼入下:

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 添加一個節(jié)點 AQS 隊列尾部
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        // 死循環(huán)
        for (;;) {
            // 找到新節(jié)點的上一個節(jié)點
            final Node p = node.predecessor();
            // 如果這個節(jié)點是 head,就嘗試獲取鎖
            if (p == head) {
                // 繼續(xù)嘗試獲取鎖儡循,這個方法是子類實現(xiàn)的
                int r = tryAcquireShared(arg);
                // 如果大于0舶吗,說明拿到鎖了。
                if (r >= 0) {
                    // 將 node 設(shè)置為 head 節(jié)點
                    // 如果大于0择膝,就說明還有機會獲取鎖誓琼,那就喚醒后面的線程,稱之為傳播
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 如果他的上一個節(jié)點不是 head肴捉,就不能獲取鎖
            // 對節(jié)點進行檢查和更新狀態(tài)腹侣,如果線程應(yīng)該阻塞,返回 true齿穗。
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 阻塞 park傲隶,并返回是否中斷,中斷則拋出異常
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            // 取消節(jié)點
            cancelAcquire(node);
    }
}

總的邏輯就是:

  1. 創(chuàng)建一個分享類型的 node 節(jié)點包裝當(dāng)前線程追加到 AQS 隊列的尾部缤灵。

  2. 如果這個節(jié)點的上一個節(jié)點是 head 伦籍,就是嘗試獲取鎖,獲取鎖的方法就是子類重寫的方法腮出。如果獲取成功了帖鸦,就將剛剛的那個節(jié)點設(shè)置成 head。

  3. 如果沒搶到鎖胚嘲,就阻塞等待作儿。

release 方法源碼分析

該方法用于釋放鎖,代碼如下:

public void release() {
    sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
    // 死循環(huán)釋放成功
    if (tryReleaseShared(arg)) {
        // 喚醒 AQS 等待對列中的節(jié)點馋劈,從 head 開始    
        doReleaseShared();
        return true;
    }
    return false;
}
// Sync extends AbstractQueuedSynchronizer 
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        // 對 state 變量 + 1
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

這里釋放鎖的邏輯寫在了抽象類 Sync 中攻锰。邏輯簡單晾嘶,就是對 state 變量做加法。

在加法成功后娶吞,執(zhí)行 doReleaseShared方法垒迂,這個方法是 AQS 的。

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                // 設(shè)置 head 的等待狀態(tài)為 0 妒蛇,并喚醒 head 上的線程
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            // 成功設(shè)置成 0 之后机断,將 head 狀態(tài)設(shè)置成傳播狀態(tài)
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

該方法的主要作用就是從 AQS 的 head 節(jié)點開始喚醒線程,注意绣夺,這里喚醒是 head 節(jié)點的下一個節(jié)點吏奸,需要和 doAcquireSharedInterruptibly方法對應(yīng),因為 doAcquireSharedInterruptibly 方法喚醒的當(dāng)前節(jié)點的上一個節(jié)點陶耍,也就是 head 節(jié)點奋蔚。

至此,釋放 state 變量烈钞,喚醒 AQS 頭節(jié)點結(jié)束泊碑。

總結(jié)

總結(jié)一下 Semaphore 的原理吧。

總的來說棵磷,Semaphore 就是一個共享鎖蛾狗,通過設(shè)置 state 變量來實現(xiàn)對這個變量的共享。當(dāng)調(diào)用 acquire 方法的時候仪媒,state 變量就減去一沉桌,當(dāng)調(diào)用 release 方法的時候,state 變量就加一算吩。當(dāng) state 變量為 0 的時候留凭,別的線程就不能進入代碼塊了,就會在 AQS 中阻塞等待偎巢。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蔼夜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子压昼,更是在濱河造成了極大的恐慌求冷,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窍霞,死亡現(xiàn)場離奇詭異匠题,居然都是意外死亡,警方通過查閱死者的電腦和手機但金,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門韭山,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事钱磅∶瘟眩” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵盖淡,是天一觀的道長年柠。 經(jīng)常有香客問我,道長禁舷,這世上最難降的妖魔是什么彪杉? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任毅往,我火速辦了婚禮牵咙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘攀唯。我一直安慰自己洁桌,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布侯嘀。 她就那樣靜靜地躺著另凌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪戒幔。 梳的紋絲不亂的頭發(fā)上吠谢,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音诗茎,去河邊找鬼工坊。 笑死,一個胖子當(dāng)著我的面吹牛敢订,可吹牛的內(nèi)容都是我干的王污。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼楚午,長吁一口氣:“原來是場噩夢啊……” “哼昭齐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起矾柜,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤阱驾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后怪蔑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體里覆,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年饮睬,在試婚紗的時候發(fā)現(xiàn)自己被綠了租谈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖割去,靈堂內(nèi)的尸體忽然破棺而出窟却,到底是詐尸還是另有隱情,我是刑警寧澤呻逆,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布夸赫,位于F島的核電站,受9級特大地震影響咖城,放射性物質(zhì)發(fā)生泄漏茬腿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一宜雀、第九天 我趴在偏房一處隱蔽的房頂上張望切平。 院中可真熱鬧,春花似錦辐董、人聲如沸悴品。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽苔严。三九已至,卻和暖如春孤澎,著一層夾襖步出監(jiān)牢的瞬間届氢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工覆旭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留退子,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓姐扮,卻偏偏與公主長得像絮供,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子茶敏,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容