歡迎來到《併發王者課》,本文是該系列文章中的第17篇。
在併發程式設計中,訊號量是執行緒同步的重要工具。在本文中,我將帶你認識訊號量的概念、用法、種類以及Java中的訊號量。
訊號量(Semaphore) 是執行緒間的同步結構,主要用於多執行緒協作時的訊號傳遞,以及對共享資源的保護、防止競態的發生等。訊號量這一概念聽起來比較抽象,然而讀完本文你會發現它竟然也是如此通俗易懂且挺有用。
一、認識簡單的訊號量
雖然訊號量的概念很抽象,但理解起來可以很簡單。比如下面這幅圖,在峽谷對局中,大喬使用大招向哪吒發起了救援,而哪吒在接收到求救訊號後前往救援。
在救援的過程中,訊號無疑是關鍵的。如果把大喬和哪吒看作兩個執行緒,那麼他們在求救、救援過程中的訊號就可以看作是訊號量,用於執行緒間的同步和通訊。
接下來,我們寫一個簡單的訊號量,模擬還原剛才的求救和施救的過程。
定義一個求救的訊號量,裡面包含訊號、訊號傳送和訊號接收。
// 求救訊號
public class ForHelpSemaphore {
private boolean signal = false;
public synchronized void sendSignal() {
this.signal = true;
this.notify();
System.out.println("呼救訊號已經傳送!");
}
public synchronized void receiveSignal() throws InterruptedException {
System.out.println("已經就緒,等待求救訊號...");
while (!this.signal) {
wait();
}
this.signal = false;
System.out.println("求救訊號已經收到,正在前往救援!");
}
}
再建立兩個執行緒,分別代表大喬和哪吒。
public static void main(String[] args) {
ForHelpSemaphore helpSemaphore = new ForHelpSemaphore();
Thread 大喬 = new Thread(helpSemaphore::sendSignal);
Thread 哪吒 = new Thread(() -> {
try {
helpSemaphore.receiveSignal();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
大喬.start();
哪吒.start();
}
從執行結果中可以看到,他們通過訊號量的機制完成了救援行動。
你看,最簡單的訊號量就是這樣的簡單。
二、理解寬泛意義上的的訊號量
如果把上面大喬和哪吒救援的例子做個梳理的話,可以發展訊號量中的一些關鍵資訊:
- 共享的資源。比如
signal
欄位是兩個執行緒共享的,它是兩個執行緒協同的基礎; - 多個執行緒訪問相同的共享資源,並根據資源狀態採取行動。比如大喬和哪吒都會讀寫
signal
欄位,然後採取行動。
基於上面的兩點理解,我們可以把訊號量抽象為下面這張圖所示:
從圖中可以看到,多個執行緒共享一份資源列表,但是資源是有限的。所以,執行緒之間必然要按照一定的順序有序地訪問資源,並在訪問結束後釋放資源。沒有獲得資源的執行緒,只能等待其他執行緒釋放資源後再次嘗試獲取。
多執行緒對共享資源的訪問過程,也可以用下面這張流程圖表示:
如果你能把這兩幅圖理解了,那麼你也就把訊號量的機制理解了。而一旦理解了機制,所謂的原始碼不過只是某種具體的實現。
三、認識不同型別的訊號量
根據訊號量的機制和應用場景,一般有下面幾種不同型別的訊號量。
1. 計數型訊號量
public class CountingSemaphore {
private int signals = 0;
public synchronized void take() {
this.signals++;
this.notify();
}
public synchronized void release() throws InterruptedException {
while (this.signals == 0)
wait();
This.signals--;
}
}
2. 邊界型訊號量
在計數型訊號量中,訊號的數量是沒有限制的。換句話說,所有的執行緒都可以傳送訊號。與此不同的是,在邊界型訊號量中,通過bound
欄位增加了訊號量的限制。
public class BoundedSemaphore {
private int signal = 0;
private int bound = 0;
public BoundedSemaphore(int upperBound) {
this.bound = upperBound;
}
public void synchronized take() throws InterruptedException {
while (this.signal == bound)
wait();
this.signal++;
this.notify++;
}
public void synchronized release() throws InterruptedException {
while (this.signal == 0)
wait();
this.signal--;
}
}
3. 定時型訊號量
定時型(timed)訊號量指的是允許執行緒在指定的時間週期內才能執行任務。時間週期結束後,定時器將會重置,所有的許可也都會被回收。
4. 二進位制型訊號量
二進位制訊號量和計數型訊號量類似,但許可的值只有0和1兩種。實現二進位制型訊號量相對也是比較容易的,如果是1就是成功,否則是0就是失敗。
四、Java中的訊號量
在理解了訊號量機制並且也理解它很有用之後,先不用著急實現它。在Java中,已經提供了相應的訊號量工具類,即java.util.concurrent.Semaphore
。並且,Java中的訊號量實現已經比較全面,你不需要再重寫它。
1. Semaphore的核心構造
Semaphore類有兩個核心構造:
Semaphore(int num)
Semaphore(int num, boolean fair)
其中,num
表示的是允許訪問共享資源的執行緒數量,而布林型別的fair
則表示執行緒等待時是否需要考慮公平。
2. Semaphore的核心方法
acquire()
: 獲取許可,如果當前沒有可用的許可,將進入阻塞等待狀態;tryAcquire()
:嘗試獲取許可,無論有沒有可用的許可,都會立即返回;release()
: 釋放許可;availablePermits()
:返回可用的許可數量。
五、如何通過訊號量實現鎖的能力
在上面的示例中,由於訊號量可以用於保護多執行緒對共享資源的訪問,所以直覺你可能會覺得它像一把鎖,而事實上訊號量確實可以用於實現鎖的能力。
比如,藉助於邊界訊號量,我們把執行緒訪問的上線設定為1,那麼此時將只有1個執行緒可以訪問共享資源,而這不就是鎖的能力嘛!
下面是通過訊號量實現鎖的一個示例:
BoundedSemaphore semaphore = new BoundedSemaphore(1);
...
semaphore.take();
try {
//臨界區
} finally {
semaphore.release();
}
我們把訊號量中的訊號數量上限設定為1,程式碼中的take()
就相當於lock()
,而release()
則相當於unlock()
。如此,訊號量搖身一變就成了名副其實的鎖。
小結
以上就是關於訊號量的全部內容。在本文中,我們介紹了訊號量的概念、執行機制、訊號量的幾種型別、Java中的訊號量實現,以及如果通過訊號量實現一把鎖。
理解訊號量的關鍵在於理解它的概念,也就是它所要解決的問題和它的方案。在理解概念和機制之後,再去看Java中的原始碼時,就會發現原來如此,又是佇列...
正文到此結束,恭喜你又上了一顆星✨
夫子的試煉
- 基於對訊號量的理解,嘗試自己實現一個簡單的訊號量。
延伸閱讀與參考資料
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。