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è)線程,以保證合理的使用公共資源朵栖。