并發(fā)編程之 Semaphore 源碼分析

前言

并發(fā) JUC 包提供了很多工具類,比如之前說的 CountDownLatch,CyclicBarrier ,今天說說這個 Semaphore——信號量,關(guān)于他的使用請查看往期文章并發(fā)編程之 線程協(xié)作工具類,今天的任務(wù)就是從源碼層面分析一下他的原理。

源碼分析

如果先不看源碼,根據(jù)以往我們看過的 CountDownLatch CyclicBarrier 的源碼經(jīng)驗來看,Semaphore 會怎么設(shè)計呢?

首先,他要實現(xiàn)多個線程線程同時訪問一個資源,類似于共享鎖,并且,要控制進入資源的線程的數(shù)量。

如果根據(jù) JDK 現(xiàn)有的資源,我們是否可以使用 AQS 的 state 變量來控制呢?類似 CountDownLatch 一樣,有幾個線程我們就為這個 state 變量設(shè)置為幾,當 state 達到了閾值,其他線程就不能獲取鎖了,就需要等待。當 Semaphore 調(diào)用 release 方法的時候,就釋放鎖,將 state 減一,并喚醒 AQS 上的線程。

以上,就是我們的猜想,那我們看看 JDK 是不是和我們想的一樣。

首先看看 Semaphore 的 UML 結(jié)構(gòu):

image.png

內(nèi)部有 3 個類,繼承了 AQS。一個公平鎖,一個非公平鎖,這點和 ReentrantLock 一摸一樣。

看看他的構(gòu)造器:

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

兩個構(gòu)造器,兩個參數(shù),一個是許可線程數(shù)量,一個是是否公平鎖,默認非公平。

而 Semaphore 有 2 個重要的方法,也是我們經(jīng)常使用的 2 個方法:

semaphore.acquire();
// doSomeing.....
semaphore.release();

acquire 和 release 方法,我們今天重點看這兩個方法的源碼,一窺 Semaphore 的全貌。

acquire 方法源碼分析

代碼如下:

public void acquire() throws InterruptedException {
    // 嘗試獲取一個鎖
    sync.acquireSharedInterruptibly(1);
}

// 這是抽象類 AQS 的方法
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 如果小于0,就獲取鎖失敗了。加入到AQS 等待隊列中。
    // 如果大于0,就直接執(zhí)行下面的邏輯了。不用進行阻塞等待。
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
// 這是抽象父類 Sync 的方法,默認是非公平的
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}
// 非公平鎖的釋放鎖的方法
final int nonfairTryAcquireShared(int acquires) {
    // 死循環(huán)
    for (;;) {
        // 獲取鎖的狀態(tài)
        int available = getState();
        int remaining = available - acquires;
        // state 變量是否還足夠當前獲取的
        // 如果小于 0,獲取鎖就失敗了。
        // 如果大于 0,就循環(huán)嘗試使用 CAS 將 state 變量更新成減去輸入?yún)?shù)之后的。
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

這里的釋放就是對 state 變量減一(或者更多)的。

返回了剩余的 state 大小。

當返回值小于 0 的時候,說明獲取鎖失敗了,那么就需要進入 AQS 的等待隊列了。代碼入下:

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 添加一個節(jié)點 AQS 隊列尾部
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        // 死循環(huán)
        for (;;) {
            // 找到新節(jié)點的上一個節(jié)點
            final Node p = node.predecessor();
            // 如果這個節(jié)點是 head,就嘗試獲取鎖
            if (p == head) {
                // 繼續(xù)嘗試獲取鎖,這個方法是子類實現(xiàn)的
                int r = tryAcquireShared(arg);
                // 如果大于0,說明拿到鎖了。
                if (r >= 0) {
                    // 將 node 設(shè)置為 head 節(jié)點
                    // 如果大于0,就說明還有機會獲取鎖,那就喚醒后面的線程,稱之為傳播
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 如果他的上一個節(jié)點不是 head,就不能獲取鎖
            // 對節(jié)點進行檢查和更新狀態(tài),如果線程應(yīng)該阻塞,返回 true。
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 阻塞 park,并返回是否中斷,中斷則拋出異常
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            // 取消節(jié)點
            cancelAcquire(node);
    }
}

總的邏輯就是:

  1. 創(chuàng)建一個分享類型的 node 節(jié)點包裝當前線程追加到 AQS 隊列的尾部。

  2. 如果這個節(jié)點的上一個節(jié)點是 head ,就是嘗試獲取鎖,獲取鎖的方法就是子類重寫的方法。如果獲取成功了,就將剛剛的那個節(jié)點設(shè)置成 head。

  3. 如果沒搶到鎖,就阻塞等待。

release 方法源碼分析

該方法用于釋放鎖,代碼如下:

public void release() {
    sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
    // 死循環(huán)釋放成功
    if (tryReleaseShared(arg)) {
        // 喚醒 AQS 等待對列中的節(jié)點,從 head 開始    
        doReleaseShared();
        return true;
    }
    return false;
}
// Sync extends AbstractQueuedSynchronizer 
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        // 對 state 變量 + 1
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

這里釋放鎖的邏輯寫在了抽象類 Sync 中。邏輯簡單,就是對 state 變量做加法。

在加法成功后,執(zhí)行 doReleaseShared方法,這個方法是 AQS 的。

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                // 設(shè)置 head 的等待狀態(tài)為 0 ,并喚醒 head 上的線程
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            // 成功設(shè)置成 0 之后,將 head 狀態(tài)設(shè)置成傳播狀態(tài)
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

該方法的主要作用就是從 AQS 的 head 節(jié)點開始喚醒線程,注意,這里喚醒是 head 節(jié)點的下一個節(jié)點,需要和 doAcquireSharedInterruptibly方法對應(yīng),因為 doAcquireSharedInterruptibly 方法喚醒的當前節(jié)點的上一個節(jié)點,也就是 head 節(jié)點。

至此,釋放 state 變量,喚醒 AQS 頭節(jié)點結(jié)束。

總結(jié)

總結(jié)一下 Semaphore 的原理吧。

總的來說,Semaphore 就是一個共享鎖,通過設(shè)置 state 變量來實現(xiàn)對這個變量的共享。當調(diào)用 acquire 方法的時候,state 變量就減去一,當調(diào)用 release 方法的時候,state 變量就加一。當 state 變量為 0 的時候,別的線程就不能進入代碼塊了,就會在 AQS 中阻塞等待。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容