線程間的同步與通信(8)——Semaphore源碼分析

前言

系列文章目錄

Semaphore(信號量)也是常用的併發工具之一,它常常用於流量控制。通常情況下,公共的資源常常是有限的,例如數據庫的連接數。使用Semaphore可以幫助我們有效的管理這些有限資源的使用。

Semaphore的結構和ReentrantLock以及CountDownLatch很像,內部採用了公平鎖與非公平鎖兩種實現,如果你已經看過了ReentrantLock源碼分析CountDownLatch源碼分析,弄懂它將毫不費力。

核心屬性

CountDownLatch類似,Semaphore主要是通過AQS的共享鎖機制實現的,因此它的核心屬性只有一個sync,它繼承自AQS:

private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 1192457210091910933L;

    Sync(int permits) {
        setState(permits);
    }

    final int getPermits() {
        return getState();
    }

    final int nonfairTryAcquireShared(int acquires) {
        //省略
    }

    protected final boolean tryReleaseShared(int releases) {
        //
    }

    final void reducePermits(int reductions) {
        //省略
    }

    final int drainPermits() {
        //省略
    }
}

這裏的permits和CountDownLatch的count很像,它們最終都將成爲AQS中的state屬性的初始值。

構造函數

Semaphore有兩個構造函數:

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

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

默認的構造函數使用的是非公平鎖,另一個構造函數通過傳入的fair參數來決定使用公平鎖還是非公平鎖,這一點和ReentrantLock用的是同樣的套路,都是同樣的代碼框架。

公平鎖和非公平鎖的定義如下:

static final class FairSync extends Sync {
    
   FairSync(int permits) {
        super(permits);
    }

    protected int tryAcquireShared(int acquires) {
        for (;;) {
            if (hasQueuedPredecessors())
                return -1;
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }
}

static final class NonfairSync extends Sync {
    
   NonfairSync(int permits) {
        super(permits);
    }

    protected int tryAcquireShared(int acquires) {
        return nonfairTryAcquireShared(acquires);
    }
}

獲取信號量

獲取信號量的方法有4個:

acquire方法 本質調用
acquire() sync.acquireSharedInterruptibly(1)
acquire(int permits) sync.acquireSharedInterruptibly(permits)
acquireUninterruptibly() sync.acquireShared(1)
acquireUninterruptibly(int permits) sync.acquireShared(permits);

可見,acquire()方法就相當於acquire(1)acquireUninterruptibly同理,只不過一種響應中斷,一種不響應中斷,關於AQS的那四個方法我們在前面的文章中都已經分析過了,除了其中的tryAcquireShared(arg)由子類實現外,其他的都由AQS實現。

值得注意的是,在逐行分析AQS源碼(3)——共享鎖的獲取與釋放中我們特別提到過tryAcquireShared返回值的含義:

  • 如果該值小於0,則代表當前線程獲取共享鎖失敗
  • 如果該值大於0,則代表當前線程獲取共享鎖成功,並且接下來其他線程嘗試獲取共享鎖的行爲很可能成功
  • 如果該值等於0,則代表當前線程獲取共享鎖成功,但是接下來其他線程嘗試獲取共享鎖的行爲會失敗

這裏的返回值其實代表的是剩餘的信號量的值,如果爲負值則說明信號量不夠了。

接下來我們就看看子類對於tryAcquireShared(arg)方法的實現:

非公平鎖實現

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;
    }
}

與一般的tryAcquire邏輯不同,Semaphore的tryAcquire邏輯是一個自旋操作,因爲Semaphore是共享鎖,同一時刻可能有多個線程來修改這個值,所以我們必須使用自旋 + CAS來避免線程衝突。

該方法退出的唯一條件是成功的修改了state值,並返回state的剩餘值。如果剩下的信號量不夠了,則就不需要進行CAS操作,直接返回剩餘值。所以其實tryAcquireShared返回的不是當前剩餘的信號量的值,而是如果扣去acquires之後,當前將要剩餘的信號量的值,如果這個“將要”剩餘的值比0小,則是不會發生扣除操作的。這就好比我要買10個包子,包子鋪現在只剩3個了,則將會返回剩餘3 - 10 = -7個包子,但是事實上包子店並沒有將包子賣出去,實際剩餘的包子還是3個;此時如果有另一個人來只要買1個包子,則將會返回剩餘3 - 1 = 2個包子,並且包子店會將一個包子賣出,實際剩餘的包子數也是2個。

非公平鎖的這種獲取信號量的邏輯其實和CountDownLatch的countDown方法很像:

// CountDownLatch
public void countDown() {
    sync.releaseShared(1);
}

countDown()releaseShared(1)方法中將調用tryReleaseShared

// CountDownLatch
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

對比CountDownLatch的tryReleaseShared方法和Semaphore的tryAcquireShared方法可知,它們的核心邏輯都是減少state的值,只不過CountDownLatch借用了共享鎖的殼,對它而言,減少state的值是一種釋放共享鎖的行爲,因爲它的目的是將state值降爲0;而在Semaphore中,減少state的值是一種獲取共享鎖的行爲,減少成功了,則獲取成功。

公平鎖實現

protected int tryAcquireShared(int acquires) {
    for (;;) {
        if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

通過對比可以看出,它和nonfairTryAcquireShared的唯一的差別在於:

if (hasQueuedPredecessors())
    return -1;

即在獲取共享鎖之前,先用hasQueuedPredecessors方法判斷有沒有人排在自己前面。關於hasQueuedPredecessors方法,我們在前面的文章中已經分析過了,它就是判斷當前節點是否有前驅節點,有的話直接返回獲取失敗,因爲要讓前驅節點先去獲取鎖。(畢竟公平鎖講究先來後到嘛)

釋放信號量

釋放信號量的方法有2個:

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

可見,release() 相當於調用了 release(1),它們最終都調用了tryReleaseShared(int releases)方法:

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");
        if (compareAndSetState(current, next))
            return true;
    }
}

與獲取信號量的邏輯相反,釋放信號量的邏輯是將得到的信號量再歸還回去,因此是增加state值的操作,代碼本身很容易理解,這裏不再贅述。

工具方法

除了以上獲取和釋放信號量所用到的方法,Semaphore還定義了一些其他方法來幫助我們操作信號量:

tryAcquire

注意,這個tryAcquire不是給acquire方法使用的!!!我們上面分析信號量的獲取時說過,獲取信號量的acquire方法調用的是AQS的acquireSharedacquireSharedInterruptibly ,而這兩個方法會調用子類的tryAcquireShared方法,子類必須實現這個方法。

而這裏的tryAcquire方法並沒有定義在AQS的子類中,即既不在NonfairSync,也不在FairSync中,對於使用共享鎖的AQS的子類,也不需要定義這個方法。事實上它直接定義在Semaphore中的。

所以,在看這個方法時,腦海中一定要有一個意識,雖然它和AQS的獨佔鎖的獲取邏輯中的tryAcquire重名了,但實際上它和AQS的獨佔鎖是沒有關係的,不要被它的名字繞暈了。

那麼,這個tryAcquiretryAcquireShared方法有什麼不同呢?只要有兩點:

  1. 返回值不同:tryAcquire返回boolean類型,tryAcquireShared返回int
  2. tryAcquire一定是採用非公平鎖模式,而tryAcquireShared有公平和非公平兩種實現。

理清楚以上幾點之後,我們再來看tryAcquire方法的源碼,它有四種重載形式:
兩種不帶超時機制的形式:

public boolean tryAcquire() {
    return sync.nonfairTryAcquireShared(1) >= 0;
}
public boolean tryAcquire(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.nonfairTryAcquireShared(permits) >= 0;
}

兩種帶超時機制的形式:

public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}

其中,不帶超時機制的tryAcquire方法實際上調用的就是nonfairTryAcquireShared(int acquires)方法,它和非公平鎖的tryAcquireShared一樣,只是tryAcquireShared是直接return nonfairTryAcquireShared(acquires),而tryAcquirereturn sync.nonfairTryAcquireShared(1) >= 0;,即直接返回獲取鎖的操作是否成功。

而帶超時機制的tryAcquire方法提供了一種超時等待的方式,這是前面介紹的公平鎖和非公平鎖的獲取鎖邏輯中所沒有的,它本質上調用了AQS的tryAcquireSharedNanos(int arg, long nanosTimeout)方法:

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquireShared(arg) >= 0 ||
        doAcquireSharedNanos(arg, nanosTimeout);
}

這個方法我們在介紹CountDownLatch源碼分析await(long timeout, TimeUnit unit)方法時已經分析過了,屬於老套路了,這裏就不展開了。

reducePermits

reducePermits方法用來減少信號量的總數,這在debug中是很有用的,它與前面介紹的acquire方法的不同點在於,即使當前信號量的值不足,它也不會導致調用它的線程阻塞等待。只要需要減少的信號量的數量reductions大於0,操作最終就會成功,也就是說,即使當前的reductions大於現有的信號量的值也沒關係,所以該方法可能會導致剩餘信號量爲負值。

protected void reducePermits(int reduction) {
    if (reduction < 0) throw new IllegalArgumentException();
    sync.reducePermits(reduction);
}
final void reducePermits(int reductions) {
    for (;;) {
        int current = getState();
        int next = current - reductions;
        if (next > current) // underflow
            throw new Error("Permit count underflow");
        if (compareAndSetState(current, next))
            return;
    }
}

我們將它和nonfairTryAcquireShared對比一下:

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

可以看出,兩者在CAS前的判斷條件並不相同,reducePermits只要剩餘值不比當前值大就可以,而nonfairTryAcquireShared必須要保證剩餘值不小於0纔會執行CAS操作。

drainPermits

相比reducePermits,drainPermits就更簡單了,它直接將剩下的信號量一次性消耗光,並且返回所消耗的信號量,這個方法在debug中也是很有用的:

public int drainPermits() {
    return sync.drainPermits();
}
final int drainPermits() {
    for (;;) {
        int current = getState();
        if (current == 0 || compareAndSetState(current, 0))
            return current;
    }
}

實戰

以上我們分析了信號量的源碼,接下來我們來分析一下官方給的一個使用的例子:

class Pool {
    private static final int MAX_AVAILABLE = 100;
    // 初始化一個信號量,設置爲公平鎖模式,總資源數爲100個
    private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);

    public Object getItem() throws InterruptedException {
        // 獲取一個信號量
        available.acquire();
        return getNextAvailableItem();
    }

    public void putItem(Object x) {
        if (markAsUnused(x))
            available.release();
    }

    // Not a particularly efficient data structure; just for demo

    protected Object[] items = ...whatever kinds of items being managed
    protected boolean[] used = new boolean[MAX_AVAILABLE];

    protected synchronized Object getNextAvailableItem() {
        for (int i = 0; i < MAX_AVAILABLE; ++i) {
            if (!used[i]) {
                used[i] = true;
                return items[i];
            }
        }
        return null; // not reached
    }

    protected synchronized boolean markAsUnused(Object item) {
        for (int i = 0; i < MAX_AVAILABLE; ++i) {
            if (item == items[i]) {
                if (used[i]) {
                    used[i] = false;
                    return true;
                } else
                    return false;
            }
        }
        return false;
    }

}

這個例子很簡單,我們用items數組代表可用的資源,用used數組來標記已經使用的資源的,used[i]的值爲true,則代表items[i]這個資源已經被使用了。

(1) 獲取一個可用資源
我們調用getItem()來獲取資源,在該方法中會先調用available.acquire()方法請求一個信號量,注意,這裏如果當前信號量數不夠時,是會阻塞等待的;當我們成功地獲取了一個信號量之後,將會調用getNextAvailableItem方法,返回一個可用的資源。

(2) 釋放一個資源
我們調用putItem(Object x)來釋放資源,在該方法中會先調用markAsUnused(Object item)將需要釋放的資源標記成可用狀態(即將used數組中對應的位置標記成false), 如果釋放成功,我們就調用available.release()來釋放一個信號量。

總結

Semaphore是一個有效的流量控制工具,它基於AQS共享鎖實現。我們常常用它來控制對有限資源的訪問。每次使用資源前,先申請一個信號量,如果資源數不夠,就會阻塞等待;每次釋放資源後,就釋放一個信號量。

(完)

系列文章目錄

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