頭條面試居然跟我扯了半小時的Semaphore
阿新 • • 發佈:2020-06-08
一個長頭髮、穿著清爽的小姐姐,拿著一個嶄新的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技術乾貨