寫在開頭
在上幾天寫《基於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 關鍵字修飾,可以給與預設實現,空方法居多,其內容邏輯由子類實現,為什麼不適用抽象方法呢?因為,抽象方法需要子類全部實現,增加大量程式碼冗餘!
【擴充套件】
- FairSync(公平模式): 呼叫 acquire() 方法的順序就是獲取許可證的順序,遵循 FIFO;
- 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哥!
如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!