1. 程式人生 > >頭條面試居然跟我扯了半小時的Semaphore

頭條面試居然跟我扯了半小時的Semaphore

一個長頭髮、穿著清爽的小姐姐,拿著一個嶄新的Mac筆記本向我走來,看著來勢洶洶,我心想著肯定是技術大佬吧!但是我也是一個才華橫溢的人,穩住我們能贏。 ![](https://img-blog.csdnimg.cn/20200607091134202.png#pic_center) > **面試官**:看你簡歷上有寫熟悉併發程式設計,Semaphore一定用過吧,跟我說說它! **我**:Semaphore是JDK提供的一個同步工具,它通過維護若干個許可證來控制執行緒對共享資源的訪問。 如果許可證剩餘數量大於零時,執行緒則允許訪問該共享資源;如果許可證剩餘數量為零時,則拒絕執行緒訪問該共享資源。 Semaphore所維護的許可證數量就是允許訪問共享資源的最大執行緒數量。 所以,執行緒想要訪問共享資源必須從Semaphore中獲取到許可證。 > **面試官**:Semaphore有哪些常用的方法? **我**:有`acquire`方法和`release`方法。 當呼叫`acquire`方法時執行緒就會被阻塞,直到Semaphore中可以獲得到許可證為止,然後執行緒再獲取這個許可證。 當呼叫`release`方法時將向Semaphore中新增一個許可證,如果有執行緒因為獲取許可證被阻塞時,它將獲取到許可證並被釋放;如果沒有獲取許可證的執行緒, Semaphore只是記錄許可證的可用數量。 歡迎關注微信公眾號:萬貓學社
,每週一分享Java技術乾貨。 > **面試官**:可以舉一個使用Semaphore的例子嗎? **我**:張三、李四和王五和趙六4個人一起去飯店吃飯,不過在特殊時期洗手很重要,飯前洗手也是必須的,可是飯店只有2個洗手池,洗手池就是不能被同時使用的公共資源,這種場景就可以用到Semaphore。 > **面試官**:可以簡單用程式碼實現一下嗎? **我**:當然可以,這是張三、李四、王五和趙六的顧客類: ```java 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(); } } } ``` 然後,寫一個測試類模擬一下他們洗手的過程: ```java 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 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的建構函式是這個: ```java public Semaphore(int permits) { sync = new NonfairSync(permits); } ``` 在這個建構函式中,使用的是NonfairSync(非公平鎖),這個類不保證執行緒獲得許可證的順序,呼叫`acquire`方法的執行緒可以在一直等待的執行緒之前獲得一個許可證。 >
**面試官**:有沒有什麼方法可保證他們的順序? **我**:可以使用Semaphore的另一個建構函式: ```java public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); } ``` 在呼叫構造方法時,`fair`引數傳入`true`,比如: ```java Semaphore washbasin = new Semaphore(2, true); ``` 這樣使用的是FairSync(公平鎖),可以確保按照各個執行緒呼叫`acquire`方法的順序獲得許可證。 > **面試官**:嗯,不錯。NonfairSync和FairSync有什麼區別?為什麼會造成這樣的效果? **我**:這就涉及到NonfairSync和FairSync的內部實現了。 在NonfairSync中,`acquire`方法核心原始碼是: ```java 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`方法核心原始碼是: ```java 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`方法核心原始碼是: ```java 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技術乾貨