頭條面試居然跟我扯了半小時的Semaphore
一個長頭髮、穿著清爽的小姐姐,拿著一個嶄新的Mac筆記型電腦向我走來,看著來勢洶洶,我心想著肯定是技術大佬吧!但是我也是一個才華橫溢的人,穩住我們能贏。

面試官:看你簡歷上有寫熟悉並發編程,Semaphore一定用過吧,跟我說說它!
我:Semaphore是JDK提供的一個同步工具,它通過維護若干個許可證來控制執行緒對共享資源的訪問。 如果許可證剩餘數量大於零時,執行緒則允許訪問該共享資源;如果許可證剩餘數量為零時,則拒絕執行緒訪問該共享資源。 Semaphore所維護的許可證數量就是允許訪問共享資源的最大執行緒數量。 所以,執行緒想要訪問共享資源必須從Semaphore中獲取到許可證。
面試官:Semaphore有哪些常用的方法?
我:有acquire方法和release方法。 當調用acquire方法時執行緒就會被阻塞,直到Semaphore中可以獲得到許可證為止,然後執行緒再獲取這個許可證。 當調用release方法時將向Semaphore中添加一個許可證,如果有執行緒因為獲取許可證被阻塞時,它將獲取到許可證並被釋放;如果沒有獲取許可證的執行緒, Semaphore只是記錄許可證的可用數量。
歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。
面試官:可以舉一個使用Semaphore的例子嗎?
我:張三、李四和王五和趙六4個人一起去飯店吃飯,不過在特殊時期洗手很重要,飯前洗手也是必須的,可是飯店只有2個洗手池,洗手池就是不能被同時使用的公共資源,這種場景就可以用到Semaphore。
面試官:可以簡單用程式碼實現一下嗎?
我:當然可以,這是張三、李四、王五和趙六的顧客類:
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();
}
}
}
然後,寫一個測試類模擬一下他們洗手的過程:
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 {
//飯店裡只用兩個洗手池,所以初始化許可證的總數為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();
}
}
}
運行以後的結果應該是這樣的:
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 趙六 洗手完畢!
可以看到,當已經有兩個人在洗手的時候,其他人就被阻塞,直到有人洗手完畢才是開始洗手。
面試官:對Semaphore的內部原理有沒有了解?
我:Semaphore內部主要通過AQS(AbstractQueuedSynchronizer)實現執行緒的管理。Semaphore在構造時,需要傳入許可證的數量,它最後傳遞給了AQS的state值。執行緒在調用acquire方法獲取許可證時,如果Semaphore中許可證的數量大於0,許可證的數量就減1,執行緒繼續運行,當執行緒運行結束調用release方法時釋放許可證時,許可證的數量就加1。如果獲取許可證時,Semaphore中許可證的數量為0,則獲取失敗,執行緒進入AQS的等待隊列中,等待被其它釋放許可證的執行緒喚醒。
歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。
面試官:嗯,不錯。在您的程式碼中,這4個人會按照執行緒啟動的順序洗手嘛?
我:不會按照執行緒啟動的順序洗手,有可能趙六比王五先洗手。
面試官:為什麼會出現這種情況?
我:因為在我的程式碼中,使用Semaphore的構造函數是這個:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
在這個構造函數中,使用的是NonfairSync(非公平鎖),這個類不保證執行緒獲得許可證的順序,調用acquire方法的執行緒可以在一直等待的執行緒之前獲得一個許可證。
面試官:有沒有什麼方法可保證他們的順序?
我:可以使用Semaphore的另一個構造函數:
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
在調用構造方法時,fair參數傳入true,比如:
Semaphore washbasin = new Semaphore(2, true);
這樣使用的是FairSync(公平鎖),可以確保按照各個執行緒調用acquire方法的順序獲得許可證。
面試官:嗯,不錯。NonfairSync和FairSync有什麼區別?為什麼會造成這樣的效果?
我:這就涉及到NonfairSync和FairSync的內部實現了。
在NonfairSync中,acquire方法核心源碼是:
final int nonfairTryAcquireShared(int acquires) {
//acquires參數默認為1,表示嘗試獲取1個許可證。
for (;;) {
int available = getState();
//remaining是剩餘的許可數數量。
int remaining = available - acquires;
//剩餘的許可數數量小於0時,
//當前執行緒進入AQS中的doAcquireSharedInterruptibly方法
//等待可用許可證並掛起,直到被喚醒。
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。
release方法核心源碼是:
protected final boolean tryReleaseShared(int releases) {
//releases參數默認為1,表示嘗試釋放1個許可證。
for (;;) {
int current = getState();
//next是如果許可證釋放成功,可用許可證的數量。
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//如果許可證釋放成功,
//當前執行緒進入到AQS的doReleaseShared方法,
//喚醒隊列中等待許可的執行緒。
if (compareAndSetState(current, next))
return true;
}
}
當一個執行緒A調用acquire方法時,會直接嘗試獲取許可證,而不管同一時刻阻塞隊列中是否有執行緒也在等待許可證,如果恰好有執行緒C調用release方法釋放許可證,並喚醒阻塞隊列中第一個等待的執行緒B,此時執行緒A和執行緒B是共同競爭可用許可證,不公平性就體現在:執行緒A沒任何等待就和執行緒B一起競爭許可證了。
而在FairSync中,acquire方法核心源碼是:
protected int tryAcquireShared(int acquires) {
//acquires參數默認為1,表示嘗試獲取1個許可證。
for (;;) {
//檢查阻塞隊列中是否有等待的執行緒
if (hasQueuedPredecessors())
return -1;
int available = getState();
//remaining是剩餘的許可數數量。
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
和非公平策略相比,FairSync中多一個對阻塞隊列是否有等待的執行緒的檢查,如果沒有,就可以參與許可證的競爭;如果有,執行緒直接被插入到阻塞隊列尾節點並掛起,等待被喚醒。
面試官:嗯,很不錯,馬上給你發offer。
本故事純屬虛構,如有雷同實屬巧合
微信公眾號:萬貓學社
微信掃描二維碼
獲得更多Java技術乾貨

