一個(gè)長(zhǎng)頭發(fā)、穿著清爽的小姐姐,拿著一個(gè)嶄新的Mac筆記本向我走來,看著來勢(shì)洶洶,我心想著肯定是技術(shù)大佬吧!但是我也是一個(gè)才華橫溢的人,穩(wěn)住我們能贏。
面試官:看你簡(jiǎn)歷上有寫熟悉并發(fā)編程,Semaphore一定用過吧,跟我說說它!
我:Semaphore是JDK提供的一個(gè)同步工具,它通過維護(hù)若干個(gè)許可證來控制線程對(duì)共享資源的訪問。
如果許可證剩余數(shù)量大于零時(shí),線程則允許訪問該共享資源;如果許可證剩余數(shù)量為零時(shí),則拒絕線程訪問該共享資源。
Semaphore所維護(hù)的許可證數(shù)量就是允許訪問共享資源的最大線程數(shù)量。 所以,線程想要訪問共享資源必須從Semaphore中獲取到許可證。
面試官:Semaphore有哪些常用的方法?
我:有acquire方法和release方法。 當(dāng)調(diào)用acquire
方法時(shí)線程就會(huì)被阻塞,直到Semaphore中可以獲得到許可證為止,然后線程再獲取這個(gè)許可證。 當(dāng)調(diào)用release
方法時(shí)將向Semaphore中添加一個(gè)許可證,如果有線程因?yàn)楂@取許可證被阻塞時(shí),它將獲取到許可證并被釋放;如果沒有獲取許可證的線程,
Semaphore只是記錄許可證的可用數(shù)量。
歡迎關(guān)注微信公眾號(hào):萬貓學(xué)社,每周一分享Java技術(shù)干貨。
面試官:可以舉一個(gè)使用Semaphore的例子嗎?
我
:張三、李四和王五和趙六4個(gè)人一起去飯店吃飯,不過在特殊時(shí)期洗手很重要,飯前洗手也是必須的,可是飯店只有2個(gè)洗手池,洗手池就是不能被同時(shí)使用的公共資源,這種場(chǎng)景就可以用到Semaphore。
面試官:可以簡(jiǎn)單用代碼實(shí)現(xiàn)一下嗎?
我:當(dāng)然可以,這是張三、李四、王五和趙六的顧客類:
package onemore.study.semaphore; import java.text.SimpleDateFormat; import
java.util.Date; import java.util.Random; import
java.util.concurrent.CountDownLatch; import java.util.concurrent.Semaphore;
public class Customer implements Runnable { private Semaphore washbasin;
private String name; public Customer(Semaphore washbasin, String name) {
this.washbasin = washbasin; this.name = name; } @Override public void run() {
try { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); Random
random = new Random(); washbasin.acquire(); System.out.println( sdf.format(new
Date()) + " " + name + " 開始洗手..."); Thread.sleep((long) (random.nextDouble() *
5000) + 2000); System.out.println( sdf.format(new Date()) + " " + name + "
洗手完畢!"); washbasin.release(); } catch (Exception e) { e.printStackTrace(); } } }
然后,寫一個(gè)測(cè)試類模擬一下他們洗手的過程:
package onemore.study.semaphore; import java.util.ArrayList; import
java.util.List; import java.util.concurrent.Semaphore; public class
SemaphoreTester { public static void main(String[] args) throws
InterruptedException { //飯店里只用兩個(gè)洗手池,所以初始化許可證的總數(shù)為2。 Semaphore washbasin = new
Semaphore(2); List<Thread> threads = new ArrayList<>(3); threads.add(new
Thread(new Customer(washbasin, "張三"))); threads.add(new Thread(new
Customer(washbasin, "李四"))); threads.add(new Thread(new Customer(washbasin,
"王五"))); threads.add(new Thread(new Customer(washbasin, "趙六"))); for (Thread
thread : threads) { thread.start(); Thread.sleep(50); } for (Thread thread :
threads) { thread.join(); } } }
運(yùn)行以后的結(jié)果應(yīng)該是這樣的:
06:51:54.416 李四 開始洗手... 06:51:54.416 張三 開始洗手... 06:51:57.251 張三 洗手完畢!
06:51:57.251 王五 開始洗手... 06:51:59.418 李四 洗手完畢! 06:51:59.418 趙六 開始洗手...
06:52:02.496 王五 洗手完畢! 06:52:06.162 趙六 洗手完畢!
可以看到,當(dāng)已經(jīng)有兩個(gè)人在洗手的時(shí)候,其他人就被阻塞,直到有人洗手完畢才是開始洗手。
面試官:對(duì)Semaphore的內(nèi)部原理有沒有了解?
我
:Semaphore內(nèi)部主要通過AQS(AbstractQueuedSynchronizer)實(shí)現(xiàn)線程的管理。Semaphore在構(gòu)造時(shí),需要傳入許可證的數(shù)量,它最后傳遞給了AQS的state值。線程在調(diào)用
acquire方法獲取許可證時(shí),如果Semaphore中許可證的數(shù)量大于0,許可證的數(shù)量就減1,線程繼續(xù)運(yùn)行,當(dāng)線程運(yùn)行結(jié)束調(diào)用release
方法時(shí)釋放許可證時(shí),許可證的數(shù)量就加1。如果獲取許可證時(shí),Semaphore中許可證的數(shù)量為0,則獲取失敗,線程進(jìn)入AQS的等待隊(duì)列中,等待被其它釋放許可證的線程喚醒。
歡迎關(guān)注微信公眾號(hào):萬貓學(xué)社,每周一分享Java技術(shù)干貨。
面試官:嗯,不錯(cuò)。在您的代碼中,這4個(gè)人會(huì)按照線程啟動(dòng)的順序洗手嘛?
我:不會(huì)按照線程啟動(dòng)的順序洗手,有可能趙六比王五先洗手。
面試官:為什么會(huì)出現(xiàn)這種情況?
我:因?yàn)樵谖业拇a中,使用Semaphore的構(gòu)造函數(shù)是這個(gè):
public Semaphore(int permits) { sync = new NonfairSync(permits); }
在這個(gè)構(gòu)造函數(shù)中,使用的是NonfairSync(非公平鎖),這個(gè)類不保證線程獲得許可證的順序,調(diào)用acquire
方法的線程可以在一直等待的線程之前獲得一個(gè)許可證。
面試官:有沒有什么方法可保證他們的順序?
我:可以使用Semaphore的另一個(gè)構(gòu)造函數(shù):
public Semaphore(int permits, boolean fair) { sync = fair ? new
FairSync(permits) : new NonfairSync(permits); }
在調(diào)用構(gòu)造方法時(shí),fair參數(shù)傳入true,比如:
Semaphore washbasin = new Semaphore(2, true);
這樣使用的是FairSync(公平鎖),可以確保按照各個(gè)線程調(diào)用acquire方法的順序獲得許可證。
面試官:嗯,不錯(cuò)。NonfairSync和FairSync有什么區(qū)別?為什么會(huì)造成這樣的效果?
我:這就涉及到NonfairSync和FairSync的內(nèi)部實(shí)現(xiàn)了。
在NonfairSync中,acquire方法核心源碼是:
final int nonfairTryAcquireShared(int acquires) {
//acquires參數(shù)默認(rèn)為1,表示嘗試獲取1個(gè)許可證。 for (;;) { int available = getState();
//remaining是剩余的許可數(shù)數(shù)量。 int remaining = available - acquires; //剩余的許可數(shù)數(shù)量小于0時(shí),
//當(dāng)前線程進(jìn)入AQS中的doAcquireSharedInterruptibly方法 //等待可用許可證并掛起,直到被喚醒。 if (remaining <
0 || compareAndSetState(available, remaining)) return remaining; } }
歡迎關(guān)注微信公眾號(hào):萬貓學(xué)社,每周一分享Java技術(shù)干貨。
release方法核心源碼是:
protected final boolean tryReleaseShared(int releases) {
//releases參數(shù)默認(rèn)為1,表示嘗試釋放1個(gè)許可證。 for (;;) { int current = getState();
//next是如果許可證釋放成功,可用許可證的數(shù)量。 int next = current + releases; if (next < current)
// overflow throw new Error("Maximum permit count exceeded"); //如果許可證釋放成功,
//當(dāng)前線程進(jìn)入到AQS的doReleaseShared方法, //喚醒隊(duì)列中等待許可的線程。 if (compareAndSetState(current,
next)) return true; } }
當(dāng)一個(gè)線程A調(diào)用acquire方法時(shí),會(huì)直接嘗試獲取許可證,而不管同一時(shí)刻阻塞隊(duì)列中是否有線程也在等待許可證,如果恰好有線程C調(diào)用release
方法釋放許可證,并喚醒阻塞隊(duì)列中第一個(gè)等待的線程B,此時(shí)線程A和線程B是共同競(jìng)爭(zhēng)可用許可證,不公平性就體現(xiàn)在:線程A沒任何等待就和線程B一起競(jìng)爭(zhēng)許可證了。
而在FairSync中,acquire方法核心源碼是:
protected int tryAcquireShared(int acquires) { //acquires參數(shù)默認(rèn)為1,表示嘗試獲取1個(gè)許可證。
for (;;) { //檢查阻塞隊(duì)列中是否有等待的線程 if (hasQueuedPredecessors()) return -1; int
available = getState(); //remaining是剩余的許可數(shù)數(shù)量。 int remaining = available -
acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return
remaining; } }
和非公平策略相比,F(xiàn)airSync中多一個(gè)對(duì)阻塞隊(duì)列是否有等待的線程的檢查,如果沒有,就可以參與許可證的競(jìng)爭(zhēng);如果有,線程直接被插入到阻塞隊(duì)列尾節(jié)點(diǎn)并掛起,等待被喚醒。
面試官:嗯,很不錯(cuò),馬上給你發(fā)offer。
本故事純屬虛構(gòu),如有雷同實(shí)屬巧合
微信公眾號(hào):萬貓學(xué)社
微信掃描二維碼
獲得更多Java技術(shù)干貨
熱門工具 換一換