Semaphore是什麼?
Semaphore中文意思是信號量,也是一個線程併發的輔助類,Semaphore實現了線程同步框架AQS,它的本質是一個"共享鎖",使用Semaphore可以控制同時訪問資源的線程個數,但是不保證線程執行順序。
Semaphore原理
Semaphore維護了有限數量的許可證,只有得到了許可證的線程才能進行共享資源的訪問,如果得不到許可證,說明當前共享資源的訪問已經達到最大限制,所以會掛起當前線程,直到前面的線程處理完任務之後,把許可證歸還,後面排隊的線程纔有機會獲取,然後處理任務。
Semaphore的構造及常用方法
構造方法:
Semaphore(int permits) //非公平模式指定最大允許訪問許可證數量
Semaphore(int permits, boolean fair)//可以通過第二個參數控制是否使用公平模式
一些常用的方法:
acquire() //申請獲取一個許可證,如果沒有許可證,就阻塞直到能夠獲取或者被打斷
availablePermits() // 返回當前有多少個有用的許可證數量hasQueuedThreads()//查詢是否有線程正在等待獲取許可證
drainPermits()//獲得並返回所有立即可用的許可證數量
getQueuedThreads()//返回一個List包含當前可能正在阻塞隊列裏面所有線程對象
getQueueLength()//返回當前可能在阻塞獲取許可證線程的數量
hasQueuedThreads()//查詢是否有線程正在等待獲取許可證
isFair()//返回是否爲公平模式
reducePermits(int reduction)//減少指定數量的許可證
reducePermits(int reduction)//釋放一個許可證
release(int permits)//釋放指定數量的許可證
tryAcquire()//非阻塞的獲取一個許可證
使用案例
Demo:模擬用戶同時訪問時,同時只能允許兩個線程執行。
public class UseSemaphore {
public static void main(String[] args) {
// 線程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能2個線程同時訪問
final Semaphore semp = new Semaphore(2);
// 模擬12個客戶端訪問
for (int index = 0; index < 6; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
System.out.println("用戶開始進入"+Thread.currentThread().getName());
// 獲取許可
semp.acquire();
System.out.println("拿到進入許可 Accessing: " + Thread.currentThread().getName());
//模擬實際業務邏輯
Thread.sleep(5000);
// 訪問完後,釋放
System.out.println("我處理完事情了,釋放許可:"+Thread.currentThread().getName());
semp.release();
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println(semp.getQueueLength());
// 退出線程池
exec.shutdown();
}
}
Semaphore底層原理:
Semaphore底層與CountDownLatch類似都是通過AQS的共享鎖機制來實現的,指定的數量會設置到AQS裏面的state裏面,然後對於每一個 調用acquire方法線程,state都會減去一,如果state等於0,那麼調用該方法的線程會被添加到同步隊列裏面,同時使用 LockSupport.park方法掛起等待,知道有線程調用了release方法,會對state加1,然後喚醒共享隊列裏面的線程,注意這裏如果是 公平模式,就直接喚醒下一個等待線程即可,如果是非公平模式就允許新加入的線程與已有的線程進行競爭,誰先得到就是誰的,如果新加入的 競爭失敗,就會走公平模式進入隊列排隊。
源碼:
1.acquire函數--獲取許可
先從獲取一個許可看起,並且先看非公平模式下的實現。首先看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;
}
}
這裏的釋放就是對 state 變量減一(或者更多)的。
返回了剩餘的 state 大小。
當返回值小於 0 的時候,說明獲取鎖失敗了,那麼就需要進入 AQS 的等待隊列了。代碼如下:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 添加一個節點 AQS 隊列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 死循環
for (;;) {
// 找到新節點的上一個節點
final Node p = node.predecessor();
// 如果這個節點是 head,就嘗試獲取鎖
if (p == head) {
// 繼續嘗試獲取鎖,這個方法是子類實現的
int r = tryAcquireShared(arg);
// 如果大於0,說明拿到鎖了。
if (r >= 0) {
// 將 node 設置爲 head 節點
// 如果大於0,就說明還有機會獲取鎖,那就喚醒後面的線程,稱之爲傳播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 如果他的上一個節點不是 head,就不能獲取鎖
// 對節點進行檢查和更新狀態,如果線程應該阻塞,返回 true。
if (shouldParkAfterFailedAcquire(p, node) &&
// 阻塞 park,並返回是否中斷,中斷則拋出異常
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
// 取消節點
cancelAcquire(node);
}
}
總的邏輯就是:
創建一個分享類型的 node 節點包裝當前線程追加到 AQS 隊列的尾部。
如果這個節點的上一個節點是 head ,就是嘗試獲取鎖,獲取鎖的方法就是子類重寫的方法。如果獲取成功了,就將剛剛的那個節點設置成 head。
如果沒搶到鎖,就阻塞等待。
看完了非公平的獲取,再看下公平的獲取,代碼如下:
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一樣首先試一把,說不定就恰好獲得了一個許可,這樣就可以插隊了。
看完了獲取許可後,再看一下release()方法。
2.release()函數--釋放許可
釋放許可也有幾個重載方法,但都會調用下面這個帶參數的方法
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()方法釋放阻塞的線程
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 設置 head 的等待狀態爲 0 ,並喚醒 head 上的線程
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 成功設置成 0 之後,將 head 狀態設置成傳播狀態
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
文章參考: