java多線程之Semaphore(信號量)

前言

本篇開始分析Semaphore(信號量)的源碼,分析結束後,會用一個示例展示Semaphore的應用場景。

1、簡介

Semaphore是一個計數信號量,維護了一個信號量許可集。每次調用acquire()都將消耗一個許可,每次調用release()都將歸還一個許可。

2、結構圖

Semaphore的內部維護了一個Sync內部類,Sync是繼承AQS的抽象類,Sync包括兩個子類:"公平信號量"FairSync 和 "非公平信號量"NonfairSync。
在這裏插入圖片描述

3、分析源碼

3.1、信號量構造函數

public Semaphore(int permits) {
	//默認構造非公平信號量
	sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
	sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

3.2、公平信號量獲取

Semaphore中的方法源碼:

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

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();
    // 嘗試獲取“共享鎖”;獲取成功則直接返回,獲取失敗,則通過doAcquireSharedInterruptibly()獲取
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

tryAcquireShared方法調用的是FairSync中的方法,源碼如下:

protected int tryAcquireShared(int acquires) {
    for (;;) {
        // 判斷當前隊列前面還有沒有等待的線程
        // 若有的話,則返回-1。
        if (hasQueuedPredecessors())
            return -1;
        // 設置“可以獲得的信號量的許可數”
        int available = getState();
        // 設置剩餘的信號量許可數
        int remaining = available - acquires;
        // 如果“剩餘的信號量許可數>=0”,則設置“可以獲得的信號量許可數”爲remaining。
        //如果“剩餘的信號量許可數小於0”,則進入doAcquireSharedInterruptibly方法
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

下面看看AQS中doAcquireSharedInterruptibly()的源碼實現,此方法在ReentrantReadWriteLock中的doAcquireShared也分析過,再看看:

private void doAcquireSharedInterruptibly(long arg)
    throws InterruptedException {
    // 創建”當前線程“的Node節點,且Node中記錄的鎖是”共享鎖“類型;並將該節點添加到CLH隊列末尾。
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            // 當前節點的前一個節點。
            final Node p = node.predecessor();
            // 如果前一個節點是頭節點(說明是第一個排隊的節點)
            if (p == head) {
            	// 再次嘗試獲取讀鎖
                long r = tryAcquireShared(arg);
                // 如果成功了
                if (r >= 0) {
                	//頭節點後移,並連續傳遞後面所有節點
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 當前線程一直等待,直到獲取到共享鎖。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

3.3、公平信號量釋放

Semaphore中的方法源碼:

public void release() {
     sync.releaseShared(1);
}
public void release(int permits) {
     if (permits < 0) throw new IllegalArgumentException();
     sync.releaseShared(permits);
}

AQS中的releaseShared方法,源碼如下:

public final boolean releaseShared(int arg) {
  	if (tryReleaseShared(arg)) {
         doReleaseShared();
         return true;
     }
     return false;
}

Sync重寫了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");
         // 如果原子更新state的值成功,就說明釋放許可成功,則返回true
         if (compareAndSetState(current, next))
             return true;
     }
}

如果tryReleaseShared()嘗試釋放共享鎖失敗,則會調用doReleaseShared()去釋放共享鎖。doReleaseShared()的源碼如下:

//此方法只會喚醒一個節點
//和ReentrantReadWriteLock中代碼一樣,不再解釋
private void doReleaseShared() {
	for (;;) {
		Node h = head;
		if (h != null && h != tail) {
			int ws = h.waitStatus;
			if (ws == Node.SIGNAL) {
				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
					continue;            // loop to recheck cases
				unparkSuccessor(h);
			}
			else if (ws == 0 &&
					 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
				continue;                // loop on failed CAS
		}
		if (h == head)                   // loop if head changed
			break;
	}
}

3.4、非公平信號量獲取和釋放

公平信號量和非公平信號量的釋放是一樣的,下面是非公平信號量的獲取源碼:

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        //這裏和公平信號量相比,少了一個判斷
        //少了的判斷是:判斷當前隊列前面還有沒有等待的線程
        //這裏就體現了公平和非公平的區別
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

4、應用場景

通過上面的分析,我們知道了Semaphore的內部維護了一個字段state,用來限制同一時間對共同資源的訪問次數,是不是非常像限流,現在我們就模擬限流場景,假設有1000個請求同時進來,我們只處理100個請求,防止服務器壓力過大,代碼如下:

public class SemaphoreLimit {

    public static final Semaphore SEMAPHORE = new Semaphore(100);
    private static final AtomicInteger SUCCESS = new AtomicInteger();

    public static void main(String[] args) {
        for (int i=0; i<1000; i++){
            new Thread(() -> { method();}).start();
        }
        System.out.printf("success request number=%d \n",SUCCESS.get());
    }
    
    public static void method(){
        if(!SEMAPHORE.tryAcquire()){
            return;
        }
        //處理業務
        try {
            Thread.sleep(100);
            SUCCESS.incrementAndGet();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            SEMAPHORE.release();
        }
    }
}

結束語

本篇分析了Semaphore源碼,瞭解了Semaphore限流的應用場景,下一篇將分析CountDownLatch。

如果本篇的內容對你有幫助,請點個贊再走,謝謝大家!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章