並發王者課-鉑金4:令行禁止-為何說信號量是線程間的同步利器


歡迎來到《並發王者課》,本文是該系列文章中的第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類有兩個核心構造:

  1. Semaphore(int num)
  2. Semaphore(int num, boolean fair)

其中,num表示的是允許訪問共享資源的線程數量,而布爾類型的fair則表示線程等待時是否需要考慮公平。

2. Semaphore的核心方法

  1. acquire(): 獲取許可,如果當前沒有可用的許可,將進入阻塞等待狀態;
  2. tryAcquire():嘗試獲取許可,無論有沒有可用的許可,都會立即返回;
  3. release(): 釋放許可;
  4. availablePermits():返回可用的許可數量。

五、如何通過信號量實現鎖的能力

在上面的示例中,由於信號量可以用於保護多線程對共享資源的訪問,所以直覺你可能會覺得它像一把鎖,而事實上信號量確實可以用於實現鎖的能力。

比如,借助於邊界信號量,我們把線程訪問的上線設置為1,那么此時將只有1個線程可以訪問共享資源,而這不就是鎖的能力嘛!

下面是通過信號量實現鎖的一個示例:

BoundedSemaphore semaphore = new BoundedSemaphore(1);
...
semaphore.take();
try {
  //臨界區
} finally {
  semaphore.release();
}

我們把信號量中的信號數量上限設置為1,代碼中的take()就相當於lock(),而release()則相當於unlock()。如此,信號量搖身一變就成了名副其實的鎖

小結

以上就是關於信號量的全部內容。在本文中,我們介紹了信號量的概念、運行機制、信號量的幾種類型、Java中的信號量實現,以及如果通過信號量實現一把鎖。

理解信號量的關鍵在於理解它的概念,也就是它所要解決的問題和它的方案。在理解概念和機制之后,再去看Java中的源碼時,就會發現原來如此,又是隊列...

正文到此結束,恭喜你又上了一顆星✨

夫子的試煉

  • 基於對信號量的理解,嘗試自己實現一個簡單的信號量。

延伸閱讀與參考資料

關於作者

關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(盡量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。

如果本文對你有幫助,歡迎點贊關注監督,我們一起從青銅到王者


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM