Java Concurrency #11: JUC — 讓多個Thread進入Crtitical section — Semaphore 信號量

Charlie Lee
Bucketing
Published in
6 min readAug 22, 2020

Concurrency問題的解決始祖,Semaphore

Photo by Shane on Unsplash

Semaphore的由來

在Concurrency Monitor(Lock)還沒出世以前,大多的程式要做互斥保護資源 ,都是使用Semaphore去做處理,直到Monitor出來之後才漸漸被取代(當然C/C++還是有許多是用Semaphore)

從字面來看的話Semaphore就是信號的意思,用在管理執行緒上,可以想像他就是個紅綠燈,當紅燈時不可行綠燈時放行,跟Monitor(Lock)開鎖解鎖的概念很像

Semaphore是由Dijkstra於1965年提出,在往後的15年都是Concurrency問題的終結者,只要使用Semaphore就可以解決大部分Multi Thread亂竄的問題,而因為他的偉大所以目前大部分主流的語言都支持Semaphore

Semaphore的底層原理

Semaphore的底層原理用個簡單的生活比喻,今天保齡球館要向櫃台借保齡球鞋,如果櫃檯沒有的話就在旁邊等待,有的話就拿一雙,結束後將鞋子歸還

Semaphore就是櫃台,程式碼簡易實踐如下

class Semaphore{ 
// 球鞋數量 (計數器)
int count;
// 等保齡球鞋隊伍 (執行緒隊伍)
Queue<Thread> q;
Semaphore(int c){
this.count=c;
}
// 向櫃台借球鞋 (執行緒像Semaphore要執行權)
void down(){

// 若沒球鞋 (若現在權限數量不構)
if(this.count<0){
// 讓玩家到旁邊排隊 (執行緒排隊)
queue.add()
// 等待
wait()
}
// 球鞋數量-- (權限數量--)
this.count--;

}

//把球鞋放回櫃檯
void up(){
//球鞋數量+1
this.count++;

//若歸還後的球鞋是櫃台唯一一雙
if(this.count<=0) {
//叫排隊中的第一個人來拿球鞋
q.poll();
// 通知玩家可以開始玩 (執行緒可以執行)
notify()
}
}
}

上方的程式碼用非常簡單的方式實踐,這只是概念上的實踐,真實的Semaphore還需要困難許多,因為Semaphore需要確保操作的原子性(不然再判斷球鞋數量和從cnt++或是q.add or poll就會導致race condition),總而言之如果要使用Java Semaphore那底層的概念就如上,只是對一個全域變數作加減,並且根據閥值判斷是否可以執行加減,而Java底層的實作已經確保Semaphore本身的操作原子性(做各種操作時不會被OS切斷轉換,會完整執行完程式碼),所以安心使用就好

Semaphore和Monitor(Lock)相比,最大的差別在於,Semaphore比較專注在入口的管理(你有資格我有空位你進來,你要走了那我就把空位讓出來),而Monitor更專注在Critical Section(你進來我就監督你直到妳出去)。

Semaphore也可以實踐出和Monitor(Lock)一模一樣的功能,只要將上述模型中的count設為1那就可以了,和Monitor一樣一次只有一個Thread可以進入Critical Section。

那有趣的事情來了,如果不將count設計為一,那就代表裡面的Critical Sectoin就不再互斥,同時會有多個Thread執行,那這也是說他不能再放Critical Code,只能放不互相影響的程式碼(不對同個資源做寫入或讀取),那這樣Semaphore還會有存在的意義嗎?

限流器

在回顧一下上面提到,Semaphore提供最大的功能是,櫃台對通行權的掌控,並且這個掌控過程是原子性(不會被OS切斷,不會被打亂順序),這樣的特性就算不當作Monitor(Lock),也可以最為可以嚴格管控執行緒進入程式碼的限流器

什麼是限流器?

如果有在寫Web CRUD的同學一定都對連線池非常熟悉,連線池的功能就是管理系統對DB的連接數量,並且這些連接就算沒用到也會保存下來不會讓他們斷線,而連線池就會需要限流器的功能,因為原子操作的特性讓系統在對連線池拿連線時一定不會有錯亂的排隊,並且一定會嚴格的遵循現在只能那這數量的連線,這也就是所謂的限流器

Semaphore範例實作

前面講的太多,先來簡單借少Semaphore的建構者與最常用的方法

Semaphore的常用方法與建構者如上,跟大部分的JUC工具一樣,都會有個treXXX的方法讓Thread不會被blocking,而最特別的是可以拿取排隊中Q的數量與明確的正在排隊Thead的物件,這幫助開發者可以在死鎖或效能不佳時有其他的B方法可以執行,也有reducePermits的方法,讓索取進入權時可以更加彈性(比如監控到CPU切換頻率太高,導致效能下降,就可以嘗試使用這方法減少Thread執行的數量)

接下來是模擬Monitor(Lock)的範例

如同使用lock一樣,在執行cnt++操作前,像Semphore拿取permits並且在結束時釋放。

限流器範例

在創建物件池時將Collection中的元素都以相同的物件塞滿(reference到同個記憶體位子),再加上semophore限制執行此元素的數量,這樣就簡單做出一個執行緒安全的連線池了(實際連線邏輯沒有在此範例實踐,只做外部管理框架)

兩個範例原始碼於此 Github連結

總結

Semaphore是早期用來確保執行緒安全的工具,就算在現在還是有許多實踐是以此為主,主要的執行邏輯,限定一個參數並且管控此參數的數量,管控的邏輯是原子性操作(再組語進入CPU時,組語不會被切斷,OS會要求CPU完全執行完此段程式碼才能Swith)。

後半段提供了兩個範例,一個是模仿Monitor(lock)的範例,只需要將管理的permits數量設定為1即可,這樣也代表只會有一個Thread進入。

另一個是限流池的範例,限流池有非常多應用,DB連線、執行緒池或是想保護那些非常需要時間與資源操作時可以使用。

--

--