這是併發執行緒工具類的第二篇文章,在第一篇中,我們分析過 CountDownLatch
的相關內容,你可以參考
那麼本篇文章我們繼續來和你聊聊併發工具類的第二篇文章 --- Semaphore 。
認識 Semaphore
Semaphore 是什麼
Semaphore 一般譯作 訊號量
,它也是一種執行緒同步工具,主要用於多個執行緒對共享資源進行並行操作的一種工具類。它代表了一種許可
的概念,是否允許多執行緒對同一資源進行操作的許可,使用 Semaphore 可以控制併發訪問資源的執行緒個數。
Semaphore 的使用場景
Semaphore 的使用場景主要用於流量控制
,比如資料庫連線,同時使用的資料庫連線會有數量限制,資料庫連線不能超過一定的數量,當連線到達了限制數量後,後面的執行緒只能排隊等前面的執行緒釋放資料庫連線後才能獲得資料庫連線。
再比如交通公路上的紅綠燈,綠燈亮起時只能讓 100 輛車通過,紅燈亮起不允許車輛通過。
再比如停車場的場景中,一個停車場有有限數量的車位,同時能夠容納多少臺車,車位滿了之後只有等裡面的車離開停車場外面的車才可以進入。
Semaphore 使用
下面我們就來模擬一下停車場的業務場景:在進入停車場之前會有一個提示牌,上面顯示著停車位還有多少,當車位為 0 時,不能進入停車場,當車位不為 0 時,才會允許車輛進入停車場。所以停車場有幾個關鍵因素:停車場車位的總容量,當一輛車進入時,停車場車位的總容量 - 1,當一輛車離開時,總容量 + 1,停車場車位不足時,車輛只能在停車場外等待。
public class CarParking {
private static Semaphore semaphore = new Semaphore(10);
public static void main(String[] args){
for(int i = 0;i< 100;i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("歡迎 " + Thread.currentThread().getName() + " 來到停車場");
// 判斷是否允許停車
if(semaphore.availablePermits() == 0) {
System.out.println("車位不足,請耐心等待");
}
try {
// 嘗試獲取
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 進入停車場");
Thread.sleep(new Random().nextInt(10000));// 模擬車輛在停車場停留的時間
System.out.println(Thread.currentThread().getName() + " 駛出停車場");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, i + "號車");
thread.start();
}
}
}
在上面這段程式碼中,我們給出了 Semaphore 的初始容量,也就是隻有 10 個車位,我們用這 10 個車位來控制 100 輛車的流量,所以結果和我們預想的很相似,即大部分車都在等待狀態。但是同時仍允許一些車駛入停車場,駛入停車場的車輛,就會 semaphore.acquire 佔用一個車位,駛出停車場時,就會 semaphore.release 讓出一個車位,讓後面的車再次駛入。
Semaphore 訊號量的模型
上面程式碼雖然比較簡單,但是卻能讓我們瞭解到一個訊號量模型的五臟六腑
。下面是一個訊號量的模型:
來解釋一下 Semaphore ,Semaphore 有一個初始容量,這個初始容量就是 Semaphore 所能夠允許的訊號量。在呼叫 Semaphore 中的 acquire 方法後,Semaphore 的容量 -1,相對的在呼叫 release 方法後,Semaphore 的容量 + 1,在這個過程中,計數器一直在監控 Semaphore 數量的變化,等到流量超過 Semaphore 的容量後,多餘的流量就會放入等待佇列中進行排隊等待。等到 Semaphore 的容量允許後,方可重新進入。
Semaphore 所控制的流量其實就是一個個的執行緒,因為併發工具最主要的研究物件就是執行緒。
它的工作流程如下
這幅圖應該很好理解吧,這裡就不再過多解釋啦。
Semaphore 深入理解
在瞭解 Semaphore 的基本使用和 Semaphore 的模型後,下面我們還是得從原始碼來和你聊一聊 Semaphore 的種種細節問題,因為我寫文章最核心的東西就是想讓我的讀者 瞭解 xxx,看這一篇就夠了,這是我寫文章的追求,好了話不多說,原始碼走起來!
Semaphore 基本屬性
Semaphore 中只有一個屬性
private final Sync sync;
Sync 是 Semaphore 的同步實現,Semaphore 保證執行緒安全性的方式和 ReentrantLock 、CountDownLatch 類似,都是繼承於 AQS 的實現。同樣的,這個 Sync 也是繼承於 AbstractQueuedSynchronizer
的一個變數,也就是說,聊 Semaphore 也繞不開 AQS,所以說 AQS 真的太重要了。
Semaphore 的公平性和非公平性
那麼我們進入 Sync 內部看看它實現了哪些方法
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
Sync(int permits) {
setState(permits);
}
final int getPermits() {
return getState();
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
final void reducePermits(int reductions) {
for (;;) {
int current = getState();
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
if (compareAndSetState(current, next))
return;
}
}
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
}
首先是 Sync 的初始化,內部呼叫了 setState
並傳遞了 permits ,我們知道,AQS 中的 State 其實就是同步狀態的值,而 Semaphore 的這個 permits 就是代表了許可的數量。
getPermits 其實就是呼叫了 getState 方法獲取了一下執行緒同步狀態值。後面的 nonfairTryAcquireShared 方法其實是在 Semaphore 中構造了 NonfairSync 中的 tryAcquireShared 呼叫的
這裡需要提及一下什麼是 NonfairSync
,除了 NonfairSync 是不是還有 FairSync 呢?查閱 JDK 原始碼發現確實有。
那麼這裡的 FairSync 和 NonfairSync 都代表了什麼?為什麼會有這兩個類呢?
事實上,Semaphore 就像 ReentrantLock 一樣,也存在“公平”和"不公平"兩種,預設情況下 Semaphore 是一種不公平的訊號量
Semaphore 的不公平意味著它不會保證執行緒獲得許可的順序,Semaphore 會線上程等待之前為呼叫 acquire 的執行緒分配一個許可,擁有這個許可的執行緒會自動將自己置於執行緒等待佇列的頭部。
當這個引數為 true 時,Semaphore 確保任何呼叫 acquire 的方法,都會按照先入先出的順序來獲取許可。
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 獲取同步狀態值
int available = getState();
// state 的值 - 當前執行緒需要獲取的訊號量(通常預設是 -1),只有
// remaining > 0 才表示可以獲取。
int remaining = available - acquires;
// 先判斷是否小於 0 ,如果小於 0 則表示無法獲取,如果是正數
// 就需要使用 CAS 判斷記憶體值和同步狀態值是否一致,然後更新為同步狀態值 - 1
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
從上面這幅原始碼對比圖可以看到,NonfairSync 和 FairSync 最大的區別就在於 tryAcquireShared
方法的區別。
NonfairSync 版本中,是不會管當前等待佇列中是否有排隊許可的,它會直接判斷訊號許可量和 CAS 方法的可行性。
FairSync 版本中,它首先會判斷是否有許可進行排隊,如果有的話就直接獲取失敗。
這時候可能就會有讀者問了,你上面說公平性和非公平性的區別一直針對的是 acquire 方法來說的,怎麼現在他們兩個主要的區別在於
tryAcquireShared
方法呢?
別急,讓我們進入到 acquire
方法一探究竟
可以看到,在 acquire 方法中,會呼叫 tryAcquireShared 方法,根據其返回值判斷是否呼叫 doAcquireSharedInterruptibly
方法,更多關於 doAcquireSharedInterruptibly 的使用分析,請參考讀者的這篇文章
這裡需要注意下,acquire 方法具有阻塞性,而 tryAcquire 方法不具有阻塞性。
這也就是說,呼叫 acquire 方法如果獲取不到許可,那麼 Semaphore 會阻塞,直到有可用的許可。而 tryAcquire 方法如果獲取不到許可會直接返回 false。
這裡還需要注意下 acquireUninterruptibly
方法,其他 acquire 的相關方法要麼是非阻塞,要麼是阻塞可中斷,而 acquireUninterruptibly 方法不僅在沒有許可的情況下執著的等待,而且也不會中斷,使用這個方法時需要注意,這個方法很容易在出現大規模執行緒阻塞而導致 Java 程式出現假死的情況。
有獲取許可相對應的就有釋放許可,但是釋放許可不會區分到底是公平釋放還是非公平釋放。不管方式如何都是釋放一個許可給 Semaphore ,同樣的 Semaphore 中的許可數量會增加。
在上圖中呼叫 tryReleaseShared 判斷是否能進行釋放後,再會呼叫 AQS 中的 releasedShared
方法進行釋放。
上面這個釋放流程只是釋放一個許可,除此之外,還可以釋放多個許可
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
後面這個 releaseShared 的釋放流程和上面的釋放流程一致。
其他 Semaphore 方法
除了上面基本的 acquire 和 release 相關方法外,我們也要了解一下 Semaphore 的其他方法。Semaphore 的其他方法比較少,只有下面這幾個
drainPermits : 獲取並退還所有立即可用的許可,其實相當於使用 CAS 方法把記憶體值置為 0
reducePermits:和 nonfairTryAcquireShared
方法類似,只不過 nonfairTryAcquireShared 是使用 CAS 使記憶體值 + 1,而 reducePermits 是使記憶體值 - 1 。
isFair:對 Semaphore 許可的爭奪是採用公平還是非公平的方式,對應到內部的實現就是 FairSync 和 NonfairSync。
hasQueuedThreads:當前是否有執行緒由於要獲取 Semaphore 許可而進入阻塞。
getQueuedThreads:返回一個包含了等待獲取許可的執行緒集合。
getQueueLength:獲取正在排隊而進入阻塞狀態的執行緒個數
我自己肝了六本 PDF,全網傳播超過10w+ ,微信搜尋「程式設計師cxuan」關注公眾號後,在後臺回覆 cxuan ,領取全部 PDF,這些 PDF 如下