初識Semaphore
- “信號量”,也可以稱其爲“信號燈”,它的存在就如同生活中的紅綠燈一般,用來控制車輛的通行。在程序員眼中,線程就好比行駛的車輛,程序員就可以通過信號量去指定線程是否可以執行,並且可以指定訪問臨界區的線程數量;
信號量模型
- 信號量的模型很簡單,有:一個計數器,一個等待隊列,三個方法(init,down,up)。在該模型中,計數器與等待隊列對外是透明的,只能去通過三個方法區訪問。
三個方法詳解:
- init():設置計數器的初始值(信號量是支持多個線程訪問一個臨界區的,所以可以通過設置計數器的初始值來控制線程訪問臨界資源的數量)
- down():計數器減一操作;如果此時計數器的值小於0,則當前線程被阻塞,否則當前線程可以繼續執行。
- up():計數器加一操作;如果此時計數器的值小於或者等於 0,則喚醒等待隊列中的一個線程,並將其從等待隊列中移除。
注意:這裏的三個方法均是原子操作。在Java SDK裏,信號量是由java.util.concurrent.Semaphore實現的,Semaphore可以保證方其都是原子操作。並且在Java SDK併發包中,down()和up()對應的是acquire()和release()方法。
參考下面代碼感受一下信號量模型:
class Semaphore{
// 計數器
int count;
// 等待隊列
Queue queue;
// 初始化操作
Semaphore(int c){
this.count=c;
}
//
void down(){
this.count--;
if(this.count<0){
// 將當前線程插入等待隊列
// 阻塞當前線程
}
}
void up(){
this.count++;
if(this.count<=0) {
// 移除等待隊列中的某個線程 T
// 喚醒線程 T
}
}
}
關於信號量的使用
1. 如何互斥操作
正如開篇介紹信號量可以控制線程的執行,相當於互斥鎖一樣,控制單一線程對臨界資源的訪問。而限號量正是通過計數器來實現互斥規則的。
如下代碼:
- 在進入臨界區之前執行down()方法,退出前執行up()方法皆就可以了。在實例代碼中acquire就相當於down方法,release相當於up方法;
static int count;
// 初始化信號量
static final Semaphore s
= new Semaphore(1);
// 用信號量保證互斥
static void addOne() {
s.acquire();
try {
count+=1;
} finally {
s.release();
}
}
信號量具體如何實現互斥?
- 比如現有兩個線程 t1與t2,而我們規定在初始化計數器時將計數器設置爲1,表明臨界區只允許一個線程去訪問。當兩個線程同時訪問addOne()方法時,兩個線程同時執行acquire()方法,由於acquire()是一個原子操作,當t1將計數器值減爲0,t2將計數器減爲-1。對於t1而言,信號量的計數器值爲0,滿足大於等於0條件,所以t1會繼續執行;對於t2而言,信號量裏邊的計數器爲-1,小於0,按照down()操作的描述,t2會被阻塞。因此只有t1線程進入臨界區執行count+=1操作。
- 當t1執行release()操作時,信號量裏邊的計數器會+1,此時爲0,小於等於0,因此會將處於等待隊列中的t2線程喚醒。於是t2在t1執行完臨界代碼之後進入到了臨界區。從而保證了互斥性。
1. Semaphore如何實現多個線程訪問一個臨界區
Semaphore有一個獨特的功能,就是可以允許多個線程訪問一個臨界區。這裏就通過“快速實現一個限流器”來說明;
- 對象池:指的是一次性創建出 N 個對象,之後所有的線程重複利用這 N 個對象,當然對象在被釋放前,也是不允許其他線程使用的。
- 這裏的限流就是指不允許多餘N個線程同時進入臨界區
解決方法:在上一個例子中我們將計數器初始化爲1,表示只允許一個線程進入臨界區,現在我們將計數器設置爲對象池中的對象個數N,表明在同一個時刻允許N個線程可以同時進入到臨界區,就可以解決限流問題了;
思考:信號量在在執行release()方法後如果滿足喚醒其他線程條件就會去喚醒一個線程繼續執行,這裏爲什麼不能去喚醒所有線程呢?
- 由於信號量沒有Condition概念,當阻塞線程被喚醒會直接運行,不會去檢查此時的臨界條件是否滿足,因此信號量只允許喚醒一個阻塞線程,否則就會出現缺少臨界條件檢查而帶來的線程安全問題。