今天我們來聊一聊Java中的Semaphore

JavaBuild發表於2024-04-13

寫在開頭

在上幾天寫《基於AQS手寫一個同步器》時,很多同學留言說裡面提到的Semaphore,講得太籠統了,今天趁著週末有空,咱們就一起詳細的學習和梳理一把 Semaphore

什麼是Semaphore?

在前面我們講過的synchronized 和 ReentrantLock 都是一次只允許一個執行緒訪問某個資源,而Semaphore(訊號量)可以用來控制同時訪問特定資源的執行緒數量,多執行緒同時操作共享資源,仍然存在著執行緒不安全問題,要想多執行緒安全,理應結合鎖進一步保障。

Semaphore的主要結構

我們跟進訊號量的原始碼中瀏覽一圈,發現其實它內部主要的方法就2個:

// 初始共享資源數量
final Semaphore semaphore = new Semaphore(5);
// 獲取1個許可
semaphore.acquire();
// 釋放1個許可
semaphore.release();

① acquire():獲取許可

跟進這個方法後,我們會發現其內部呼叫了AQS的一個final 方法acquireSharedInterruptibly(),這個方法中又呼叫了tryAcquireShared(arg)方法,作為AQS中的鉤子方法,這個方法的實現在Semaphore的兩個靜態內部類 FairSync(公平模式)NonfairSync(非公平模式) 中。

【原始碼解析1】

/**
 *  獲取1個許可證
 */
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
/**
 * 共享模式下獲取許可證,獲取成功則返回,失敗則加入阻塞佇列,掛起執行緒
 */
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
      throw new InterruptedException();
        // 嘗試獲取許可證,arg為獲取許可證個數,當可用許可證數減當前獲取的許可證數結果小於0,則建立一個節點加入阻塞佇列,掛起當前執行緒。
    if (tryAcquireShared(arg) < 0)
      doAcquireSharedInterruptibly(arg);
}

繼續跟入tryAcquireShared(arg)方法,雖然它在AQS中,但它作為鉤子方法,最終的實現則回到了Semaphore的內部類中。

鉤子方法: 一種抽象類中的方法,一般使用 protected 關鍵字修飾,可以給與預設實現,空方法居多,其內容邏輯由子類實現,為什麼不適用抽象方法呢?因為,抽象方法需要子類全部實現,增加大量程式碼冗餘!

image

【擴充套件】

  1. FairSync(公平模式): 呼叫 acquire() 方法的順序就是獲取許可證的順序,遵循 FIFO;
  2. NonfairSync(非公平模式): 搶佔式的,在構造方法中透過一個布林值fair來配置公平與非公平,預設為非公平模式。

② release():釋放許可

同樣跟入這個方法,裡面用了AQS的releaseShared(),而在這個方法內也毫無疑問的用了tryReleaseShared(int arg)這個鉤子方法,原理同上,不再冗釋,需要注意的是釋放共享鎖的同時也會喚醒同步佇列中的一個執行緒。

【原始碼解析2】

// 釋放一個許可證
public void release() {
    sync.releaseShared(1);
}

// 釋放共享鎖,同時會喚醒同步佇列中的一個執行緒。
public final boolean releaseShared(int arg) {
    //釋放共享鎖
    if (tryReleaseShared(arg)) {
      //喚醒同步佇列中的一個執行緒
      doReleaseShared();
      return true;
    }
    return false;
}

Semaphore的使用

OK,講到這裡,把訊號量中主要的方法解釋完了,我們來寫一個小demo感受一下它的使用:

【測試用例1】

public class Test {
    private final Semaphore semaphore;

    /*構造一個令牌*/
    public Test(int acq){
        this.semaphore= new Semaphore(acq);
    }
    public void useSemaphore(){
        try {
            semaphore.acquire();
            // 使用資源
            System.out.println("資源開始使用了 " + Thread.currentThread().getName());
            Thread.sleep(1000); // 模擬資源使用時間
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
            System.out.println("資源釋放了 " + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        Test test = new Test(3);
        for (int i = 0; i < 5; i++) {
            new Thread(test::useSemaphore).start();
        }
    }
}

輸出:

資源開始使用了 Thread-0
資源開始使用了 Thread-1
資源開始使用了 Thread-3
資源釋放了 Thread-0
資源開始使用了 Thread-2
資源開始使用了 Thread-4
資源釋放了 Thread-1
資源釋放了 Thread-3
資源釋放了 Thread-4
資源釋放了 Thread-2

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!

image

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!

image

相關文章