Java并發(fā)編程 - Semaphore(信號(hào)機(jī))

Semaphore在中文的書(shū)籍里面把它翻譯成"信號(hào)量"陡蝇。說(shuō)實(shí)在話,在很長(zhǎng)的時(shí)間我都無(wú)法從這個(gè)翻譯上很直觀得明白它到底是個(gè)什么東西踩萎。"信號(hào)的數(shù)量"正罢? 什么鬼?!

字典里的解釋是: 臂板信號(hào)系統(tǒng)翻具,(鐵道)臂板信號(hào)裝置履怯。它是一種系統(tǒng)或裝置,嗯呢裆泳,好像有點(diǎn)明白了叹洲。

在學(xué)習(xí)與使用了這個(gè)類(lèi)之后,我更喜歡將它翻譯成——信號(hào)機(jī)或信號(hào)管理器工禾。

在編程世界里提出這個(gè)概念是用來(lái)解決對(duì)代碼臨界區(qū)進(jìn)行訪問(wèn)控制的問(wèn)題运提。臨界區(qū)是受保護(hù)的,它里面包含了對(duì)敏感資源的操作闻葵,多線程環(huán)境下需要對(duì)這片區(qū)域加以控制民泵,以保證安全地?zé)o誤得操作資源。

Semaphore是一個(gè)信號(hào)機(jī)槽畔,它可以產(chǎn)生信號(hào)(把它看作一個(gè)實(shí)體)栈妆,線程得到信號(hào)就表示它有權(quán)進(jìn)入臨界區(qū),當(dāng)線程執(zhí)行完臨界區(qū)內(nèi)的操作厢钧,退出臨界區(qū)鳞尔,信號(hào)會(huì)被回收以使得信號(hào)機(jī)可以將它重新分配給另一個(gè)想進(jìn)入臨界區(qū)的線程。

Semaphore工作原理可以類(lèi)比于下面的場(chǎng)景:

Semaphore是澡堂管理員早直,它手上有有限數(shù)量的澡牌寥假,每個(gè)要洗澡的人過(guò)來(lái),首先需要到澡堂管理員這里來(lái)獲取澡牌霞扬,只有得到澡牌才能進(jìn)入澡堂洗澡糕韧,洗完澡后出來(lái)要把澡牌還給澡堂管理員;澡牌發(fā)完之后喻圃,后面來(lái)洗澡的人就需要在澡堂門(mén)口持續(xù)等待萤彩,直到澡堂管理員手上有回收回來(lái)的澡牌。

類(lèi)比這個(gè)場(chǎng)景级及,Semaphore中應(yīng)該包含的邏輯乒疏,用偽代碼可以按照如下書(shū)寫(xiě):

value; // 澡牌的數(shù)量

// 請(qǐng)求澡牌
acquire() {
    while value <=0 
        ;// 現(xiàn)在沒(méi)有澡牌了,只有等了
     value--; // 澡牌的數(shù)量-1
}

// 歸還澡牌
release() {
    value++饮焦;// 澡牌的數(shù)量+1
}

Java語(yǔ)言提供了java.util.concurrent.Semaphore類(lèi)怕吴,它是信號(hào)機(jī)機(jī)制的實(shí)現(xiàn)類(lèi)。

澡堂洗澡場(chǎng)景-1

我們通過(guò)使用java.util.concurrent.Semaphore類(lèi)來(lái)實(shí)現(xiàn)澡堂洗澡這個(gè)場(chǎng)景的模擬县踢。

Bathhouse.java

import java.util.concurrent.Semaphore;

public class Bathhouse {
    
    private static final Semaphore bathhouseManager = new Semaphore(10);// 澡堂管理員手上有10張?jiān)枧?    
    public void bathe() {
        try {
            bathhouseManager.acquire();
            System.out.println(Thread.currentThread().getName() + ": 在洗澡...");
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() + ": 出澡堂...");
            bathhouseManager.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
    }
    
    public static void main(String[] args) {
        Bathhouse bathhouse = new Bathhouse(); 
        
        for (int i=1; i<=100; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    bathhouse.bathe();
                }
                
            }, "洗澡君" + i).start();
        }
    }

}

上面這句代碼:

private static final Semaphore bathhouseManager = new Semaphore(10);

表示澡堂管理員手上只有10個(gè)澡牌转绷,也就是表示只有10個(gè)澡位。

運(yùn)行代碼我們可以發(fā)現(xiàn)硼啤,運(yùn)行后立即會(huì)輸出10條"在洗澡..."語(yǔ)句议经,這表示一次性可以同時(shí)有10個(gè)洗澡的人得到澡牌進(jìn)入澡堂,后面會(huì)斷斷續(xù)續(xù)得輸出。

浴室洗澡場(chǎng)景-1

你家來(lái)了10位朋友煞肾,但是浴室只有一個(gè)咧织,假設(shè)你們正在玩撲克,有一張撲克代表是洗澡牌籍救,抽到洗澡牌的朋友就可以去洗澡习绢,洗完澡之后把洗澡牌歸還,然后繼續(xù)抽牌蝙昙,抽到的朋友可以去洗澡闪萄,如此反復(fù)......

還是使用java.util.concurrent.Semaphore類(lèi)來(lái)模擬這個(gè)場(chǎng)景。

Bathroom.java

import java.util.concurrent.Semaphore;

public class Bathroom {
    
    private static final Semaphore poker = new Semaphore(1);// 一副撲克里面只有一張洗澡牌
    
    public void bathe() {
        try {
            poker.acquire();
            System.out.println(Thread.currentThread().getName() + ": 在洗澡...");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + ": 出浴室...");
            poker.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
    }
    
    public static void main(String[] args) {
        Bathroom bathroom = new Bathroom(); 
        
        for (int i=1; i<=10; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    bathroom.bathe();
                }
                
            }, "朋友-" + i).start();
        }
    }

}

運(yùn)行程序我們可以發(fā)現(xiàn)你的朋友是一個(gè)一個(gè)去洗澡的奇颠。

浴室洗澡場(chǎng)景-2

你家來(lái)了2個(gè)朋友A和B败去,由你規(guī)定只有A先洗完澡B才能去洗。模擬代碼如下:

Bathroom.java

import java.util.concurrent.Semaphore;

public class Bathroom {
    
    public static void main(String[] args) {
        
        final Semaphore poker = new Semaphore(0);
        
        // A線程
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    poker.acquire();
                    System.out.println(Thread.currentThread().getName() + ": 在洗澡...");
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName() + ": 出浴室...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            
        }, "朋友-B").start();
        
        // B線程
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 在洗澡...");
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName() + ": 出浴室...");
                    poker.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            
        }, "朋友-A").start();
        
    }

}

運(yùn)行結(jié)果烈拒,輸出的結(jié)果是我們預(yù)期的圆裕。

上面的代碼我們可以看到acquire()和release()是分開(kāi)到兩個(gè)線程內(nèi)部的葫辐。這樣分開(kāi)后就可以實(shí)現(xiàn)朋友A先洗朋友B后洗的需求。

回到代碼中來(lái):

final Semaphore poker = new Semaphore(0);

這里按照我們剛才的說(shuō)法是一張洗澡牌都沒(méi)有焊傅,朋友B去請(qǐng)求獲取洗澡牌的時(shí)候不會(huì)得到洗澡牌狐胎,所以就算朋友B首先提出了要先洗握巢,也是洗不了的暴浦。但是你的朋友A很特殊歌焦,她是你的女朋友独撇,她不用向你申請(qǐng)洗澡牌就可以去洗澡,而且她希望澡之后還獲得了一張洗澡牌卵史,她給了朋友B搜立,朋友B就可以去洗澡儒拂。

上面的說(shuō)明可能有點(diǎn)牽強(qiáng),這里本來(lái)不是介紹java.util.concurrent.Semaphore的源碼實(shí)現(xiàn)的见转,不過(guò)為了對(duì)這個(gè)例子進(jìn)行解釋蒜哀,還是來(lái)進(jìn)行簡(jiǎn)單的說(shuō)明撵儿。

java.util.concurrent.Semaphore內(nèi)部維護(hù)了一個(gè)變量(在AQS類(lèi)中定義的淀歇,這里我們就簡(jiǎn)單地理解為是它維護(hù)的):

private volatile int state;

表示Semaphore這個(gè)信號(hào)機(jī)內(nèi)部擁有的信號(hào)的數(shù)量(哈哈浪默,信號(hào)量)碰逸。

acquire()方法調(diào)用會(huì)申請(qǐng)獲取一個(gè)信號(hào)饵史,

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

如果信號(hào)量不夠(我們這里就是state=0)胳喷,也就是說(shuō)state-1< 0厌蔽,那么信號(hào)獲取失敗,線程被加入到等待隊(duì)列中摔癣。

這里一定要注意:信號(hào)量不夠不一定是指state=0纬向,而是信號(hào)機(jī)沒(méi)有那么多信號(hào)給你,比如說(shuō)你申請(qǐng)3個(gè)逾条,但是現(xiàn)在state=1,只有一個(gè)信號(hào)师脂,無(wú)法滿足你的申請(qǐng)需求。所以程序中判斷是否能夠滿足你的申請(qǐng)要求是通過(guò)【state-你的申請(qǐng)量】是否大于0來(lái)決定的江锨。

acquire()方法是無(wú)參方法吃警,默認(rèn)就申請(qǐng)一個(gè)信號(hào)啄育。

上面線程B先運(yùn)行挑豌,調(diào)用acquire() 方法的時(shí)候因?yàn)楫?dāng)前的state=0,所以無(wú)法獲取到信號(hào),被加入到等待隊(duì)列中址貌。

線程A執(zhí)行到release()方法,會(huì)釋放一個(gè)信號(hào)余舶,那么此時(shí)state=1,并且會(huì)喚醒等待隊(duì)列中等待的B線程匿值。

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

B線程被喚醒繼續(xù)執(zhí)行赠制,這時(shí)候因?yàn)閟tate=1了,有了1個(gè)信號(hào)挟憔,而B(niǎo)請(qǐng)求的也是1個(gè)信號(hào)钟些,剛好滿足,朋友B就能去洗澡了绊谭。

澡堂洗澡場(chǎng)景-2

為了加深對(duì)java.util.concurrent.Semaphore的使用政恍,我們繼續(xù)來(lái)看一個(gè)場(chǎng)景。

場(chǎng)景:有個(gè)暴發(fā)戶去洗澡达传,他要包場(chǎng)篙耗,不過(guò)他提前沒(méi)打招呼迫筑,他過(guò)來(lái)洗的時(shí)候有人已經(jīng)進(jìn)去洗了,所以他要等里面的人全洗完宗弯,再去洗脯燃。

也就是說(shuō)暴發(fā)戶要一次性請(qǐng)求10張洗澡牌,請(qǐng)求成功后他才進(jìn)去洗蒙保。

Bathhouse.java

import java.util.concurrent.Semaphore;

public class Bathhouse {
    
    public static void main(String[] args) throws InterruptedException {
        
        Semaphore bathhouseManager = new Semaphore(10);
         
        // 線程A
        Thread threadA = new Thread(new Runnable(){

            @Override
            public void run() {
                try {
                    
                    Thread.sleep(2000);// 避免暴發(fā)戶一開(kāi)始就能拿到10張?jiān)枧?                    
                    boolean flag = false;
                    
                    while (!flag) {
                        flag = bathhouseManager.tryAcquire(10);
                        System.out.println("暴發(fā)戶在等澡牌...");
                    }
                    
                    System.out.println("***暴發(fā)戶進(jìn)入洗澡了...***");
                    Thread.sleep(2000);
                    System.out.println("***暴發(fā)戶出澡堂了...***");
                    
                    /**
                     * 暴發(fā)戶只歸還了一張?jiān)枧?                     */
                    bathhouseManager.release(1);
                    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            
        });
        threadA.start();
        
        for (int i=1; i<=10; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        bathhouseManager.acquire();
                        System.out.println(Thread.currentThread().getName() + ": 在洗澡...");
                        Thread.sleep(3000);
                        System.out.println(Thread.currentThread().getName() + ": 出澡堂...");
                        bathhouseManager.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
            }, "洗澡君-" + i).start();
        }
        
        threadA.join();
        
        
        System.out.println("***暴發(fā)戶拿走了" + (10 - bathhouseManager.availablePermits()) + "張?jiān)枧?..***");
        
    }
    

}

運(yùn)行上面的程序辕棚,輸出的結(jié)果可以保證:所有的洗澡君洗完之后,暴發(fā)戶才會(huì)去洗邓厕,否則他會(huì)一直等待洗澡牌滿10張逝嚎。

***暴發(fā)戶進(jìn)入洗澡了...***
***暴發(fā)戶出澡堂了...***
***暴發(fā)戶拿走了9張?jiān)枧?..***

這三行在最后末尾輸出。

我們上面通過(guò)相關(guān)的場(chǎng)景講的是java.util.concurrent.Semaphore這個(gè)信號(hào)機(jī)的使用详恼,也說(shuō)明了一下基本的原理懈糯,但是有些關(guān)于實(shí)現(xiàn)的問(wèn)題沒(méi)有深入源碼進(jìn)行講解。

比如說(shuō)請(qǐng)求獲取信號(hào)失敗后當(dāng)前線程怎么辦单雾?

對(duì)于這個(gè)問(wèn)題赚哗,其中的一種解決辦法是讓當(dāng)前線程一直循環(huán)獲取,也就是說(shuō)自旋硅堆,這樣不用掛起線程屿储,然后再喚起線程,導(dǎo)致不必要的上下文切換的開(kāi)銷(xiāo)渐逃。不過(guò)自旋表示線程還在運(yùn)行够掠,這樣還是會(huì)占用CPU的,如果其他獲取到信號(hào)的線程執(zhí)行任務(wù)的時(shí)候不是很長(zhǎng)茄菊,也就是說(shuō)能很快地能從臨界區(qū)中退出疯潭,那么自旋效果還是挺好的。

還有另外一種解決辦法就是在信號(hào)機(jī)中維護(hù)一個(gè)隊(duì)列面殖,將信號(hào)獲取失敗的線程放入到隊(duì)列中以等待下次喚醒竖哩。這就好比沒(méi)有澡牌后,澡堂管理員將要洗澡的人帶到休息等待區(qū)脊僚,等有澡牌后就去叫他們相叁,將澡牌再分配。當(dāng)然了辽幌,這時(shí)候可能有非等待區(qū)的人到來(lái)了增淹,這個(gè)時(shí)候澡牌如何分配,是先來(lái)后到乌企,還是懶得去叫等待區(qū)的人虑润,而是讓新來(lái)的人進(jìn)去洗,這就又涉及到一個(gè)公平和非公平分配的問(wèn)題了加酵,這個(gè)是后話了拳喻。

java.util.concurrent.Semaphore基于java.util.concurrent.locks.AbstractQueuedSynchronizer梁剔,這里我們不會(huì)深入源碼,會(huì)有專門(mén)的文章講解AQS舞蔽,把AQS講通了荣病,信號(hào)機(jī)內(nèi)部具體是怎么實(shí)現(xiàn)的也就清楚了。

好了渗柿,最后通過(guò)JDK API中對(duì)Semaphore作用的描述來(lái)結(jié)束本篇文章:

Semaphore(信號(hào)機(jī))是用來(lái)控制同時(shí)訪問(wèn)特定資源的線程數(shù)量个盆,它通過(guò)協(xié)調(diào)各個(gè)線程,以保證合理的使用公共資源朵栖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末颊亮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子陨溅,更是在濱河造成了極大的恐慌终惑,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件门扇,死亡現(xiàn)場(chǎng)離奇詭異雹有,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)臼寄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)霸奕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人吉拳,你說(shuō)我怎么就攤上這事质帅。” “怎么了留攒?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵煤惩,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我炼邀,道長(zhǎng)魄揉,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任汤善,我火速辦了婚禮什猖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘红淡。我一直安慰自己,他們只是感情好降铸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布在旱。 她就那樣靜靜地躺著,像睡著了一般推掸。 火紅的嫁衣襯著肌膚如雪桶蝎。 梳的紋絲不亂的頭發(fā)上驻仅,一...
    開(kāi)封第一講書(shū)人閱讀 51,698評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音登渣,去河邊找鬼噪服。 笑死,一個(gè)胖子當(dāng)著我的面吹牛胜茧,可吹牛的內(nèi)容都是我干的粘优。 我是一名探鬼主播,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼呻顽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼雹顺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起廊遍,我...
    開(kāi)封第一講書(shū)人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嬉愧,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后喉前,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體没酣,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年卵迂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了四康。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡狭握,死狀恐怖闪金,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情论颅,我是刑警寧澤哎垦,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站恃疯,受9級(jí)特大地震影響漏设,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜今妄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一郑口、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧盾鳞,春花似錦犬性、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春姨夹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抄课。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工稿黄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留喊衫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓杆怕,卻偏偏與公主長(zhǎng)得像族购,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子财著,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

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