什麼是Semaphore?
Semaphore是計數訊號量。Semaphore管理一系列許可證。每個acquire方法阻塞,直到有一個許可證可以獲得然後拿走一個許可證;每個release方法增加一個許可證,這可能會釋放一個阻塞的acquire方法。然而,其實並沒有實際的許可證這個物件,Semaphore只是維持了一個可獲得許可證的數量。
應用場景
Semaphore可以用於做流量控制,特別公用資源有限的應用場景,比如資料庫連線。假如有一個需求,要讀取幾萬個檔案的資料,因為都是IO密集型任務,我們可以啟動幾十個執行緒併發的讀取,但是如果讀到記憶體後,還需要儲存到資料庫中,而資料庫的連線數只有10個,這時我們必須控制只有十個執行緒同時獲取資料庫連線儲存資料,否則會報錯無法獲取資料庫連線。這個時候,我們就可以使用Semaphore來做流控,程式碼如下:
package org.java.base.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;public class SemaphoreTest {
private static final int THREAD_COUNT = 30;
private static ExecutorService threadPool = Executors
.newFixedThreadPool(THREAD_COUNT);private static Semaphore s = new Semaphore(10);
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
s.acquire();
System.out.println(“save data”);
s.release();
} catch (InterruptedException e) {
}
}
});
}threadPool.shutdown();
}
}
在程式碼中,雖然有30個執行緒在執行,但是隻允許10個併發的執行。Semaphore的構造方法Semaphore(int permits) 接受一個整型的數字,表示可用的許可證數量。Semaphore(10)表示允許10個執行緒獲取許可證,也就是最大併發數是10。Semaphore的用法也很簡單,首先執行緒使用Semaphore的acquire()獲取一個許可證,使用完之後呼叫release()歸還許可證。還可以用tryAcquire()方法嘗試獲取許可證。
其他方法
Semaphore還提供一些其他方法:
- int availablePermits() :返回此訊號量中當前可用的許可證數。
- int getQueueLength():返回正在等待獲取許可證的執行緒數。
- boolean hasQueuedThreads() :是否有執行緒正在等待獲取許可證。
- void reducePermits(int reduction) :減少reduction個許可證。是個protected方法。
- Collection getQueuedThreads() :返回所有等待獲取許可證的執行緒集合。是個protected方法。
原始碼解析
Semaphore有兩種模式,公平模式和非公平模式。公平模式就是呼叫acquire的順序就是獲取許可證的順序,遵循FIFO;而非公平模式是搶佔式的,也就是有可能一個新的獲取執行緒恰好在一個許可證釋放時得到了這個許可證,而前面還有等待的執行緒。
構造方法
Semaphore有兩個構造方法,如下:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}複製程式碼
從上面可以看到兩個構造方法,都必須提供許可的數量,第二個構造方法可以指定是公平模式還是非公平模式,預設非公平模式。
Semaphore內部基於AQS的共享模式,所以實現都委託給了Sync類。
這裡就看一下NonfairSync的構造方法:
NonfairSync(int permits) {
super(permits);
}複製程式碼
可以看到直接呼叫了父類的構造方法,Sync的構造方法如下:
Sync(int permits) {
setState(permits);
}複製程式碼
可以看到呼叫了setState方法,也就是說AQS中的資源就是許可證的數量。
獲取許可
先從獲取一個許可看起,並且先看非公平模式下的實現。首先看acquire方法,acquire方法有幾個過載,但主要是下面這個方法
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}複製程式碼
從上面可以看到,呼叫了Sync的acquireSharedInterruptibly方法,該方法在父類AQS中,如下:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//如果執行緒被中斷了,丟擲異常
if (Thread.interrupted())
throw new InterruptedException();
//獲取許可失敗,將執行緒加入到等待佇列中
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}複製程式碼
AQS子類如果要使用共享模式的話,需要實現tryAcquireShared方法,下面看NonfairSync的該方法實現:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}複製程式碼
該方法呼叫了父類中的nonfairTyAcquireShared方法,如下:
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//獲取剩餘許可數量
int available = getState();
//計算給完這次許可數量後的個數
int remaining = available - acquires;
//如果許可不夠或者可以將許可數量重置的話,返回
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}複製程式碼
從上面可以看到,只有在許可不夠時返回值才會小於0,其餘返回的都是剩餘許可數量,這也就解釋了,一旦許可不夠,後面的執行緒將會阻塞。看完了非公平的獲取,再看下公平的獲取,程式碼如下:
protected int tryAcquireShared(int acquires) {
for (;;) {
//如果前面有執行緒再等待,直接返回-1
if (hasQueuedPredecessors())
return -1;
//後面與非公平一樣
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}複製程式碼
從上面可以看到,FairSync與NonFairSync的區別就在於會首先判斷當前佇列中有沒有執行緒在等待,如果有,就老老實實進入到等待佇列;而不像NonfairSync一樣首先試一把,說不定就恰好獲得了一個許可,這樣就可以插隊了。
看完了獲取許可後,再看一下釋放許可。
釋放許可
釋放許可也有幾個過載方法,但都會呼叫下面這個帶引數的方法,
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}複製程式碼
releaseShared方法在AQS中,如下:
public final boolean releaseShared(int arg) {
//如果改變許可數量成功
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}複製程式碼
AQS子類實現共享模式的類需要實現tryReleaseShared類來判斷是否釋放成功,實現如下:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
//獲取當前許可數量
int current = getState();
//計算回收後的數量
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//CAS改變許可數量成功,返回true
if (compareAndSetState(current, next))
return true;
}
}複製程式碼
從上面可以看到,一旦CAS改變許可數量成功,那麼就會呼叫doReleaseShared()方法釋放阻塞的執行緒。
減小許可數量
Semaphore還有減小許可數量的方法,該方法可以用於用於當資源用完不能再用時,這時就可以減小許可證。程式碼如下:
protected void reducePermits(int reduction) {
if (reduction < 0) throw new IllegalArgumentException();
sync.reducePermits(reduction);
}複製程式碼
可以看到,委託給了Sync,Sync的reducePermits方法如下:
final void reducePermits(int reductions) {
for (;;) {
//得到當前剩餘許可數量
int current = getState();
//得到減完之後的許可數量
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
//如果CAS改變成功
if (compareAndSetState(current, next))
return;
}
}複製程式碼
從上面可以看到,就是CAS改變AQS中的state變數,因為該變數代表許可證的數量。
獲取剩餘許可數量
Semaphore還可以一次將剩餘的許可數量全部取走,該方法是drain方法,如下:
public int drainPermits() {
return sync.drainPermits();
}複製程式碼
Sync的實現如下:
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}複製程式碼
可以看到,就是CAS將許可數量置為0。
總結
Semaphore是訊號量,用於管理一組資源。其內部是基於AQS的共享模式,AQS的狀態表示許可證的數量,在許可證數量不夠時,執行緒將會被掛起;而一旦有一個執行緒釋放一個資源,那麼就有可能重新喚醒等待佇列中的執行緒繼續執行。