Semaphore原始碼分析

辣雞小籃子發表於2020-09-10

Semaphore原始碼分析

Semaphore的作用是在有限的資源裡,當有N個執行緒進行訪問時,如何進行資源的分配。

基本的使用方法是,建立一個Semaphore例項,並且指定permit的個數,當執行緒要獲取許可時呼叫acquire()方法,當釋放許可時呼叫release()方法,當沒有可用的許可時,執行緒將會進行阻塞。

Semaphore是基於AQS進行實現的,所以它底層肯定是通過自定義AQS共享模式下的同步器來實現的,該同步器需要重寫AQS提供的tryAcquireShared()和tryReleaseShared()方法,只需要告訴AQS是否嘗試獲取同步資源和釋放同步資源成功即可。

AQS子類需要定義以及維護同步狀態的值,在Semaphore中,同步狀態state的值為同步資源的個數。


Semaphore的結構

public class Semaphore implements java.io.Serializable {
    private static final long serialVersionUID = -3222578661600680210L;
    
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
       // ......
    }

    /**
     * NonFair version
     */
    static final class NonfairSync extends Sync {
		// ......        
    }

    /**
     * Fair version
     */
    static final class FairSync extends Sync {
        // ......
    }

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

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

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

    public boolean tryAcquire() {
        return sync.nonfairTryAcquireShared(1) >= 0;
    }

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

    public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
    }

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

    public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }
    
    /**
     * 返回可用的許可
     */
    public int availablePermits() {
        return sync.getPermits();
    }
    
    /**
     * 耗盡許可
     */
    public int drainPermits() {
        return sync.drainPermits();
    }

    /**
     * 減少許可
     */
    protected void reducePermits(int reduction) {
        if (reduction < 0) throw new IllegalArgumentException();
        sync.reducePermits(reduction);
    }

    // 其他省略
}

可以看到Semaphore中定義了一個抽象同步器(Sync)、非公平同步器(NonfairSync)和公平同步器(FairSync),同時非公平同步器和公平同步器都繼承抽象同步器。

同時Semaphore中存在一個全域性的抽象同步器屬性,然後通過構造方法來進行初始化,通過fair引數來指定到底是使用非公平的還是公平的同步器(預設非公平),通過permits引數來指定同步資源的個數。

同時Semaphore的acquire()方法將會直接呼叫AQS的acquireSharedInterruptibly()方法,tryAcquire()方法呼叫抽象同步器的nonfairTryAcquireShared()方法,release()方法直接呼叫AQS的releaseShared()方法。


剖析抽象同步器

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) {
        // 死迴圈保證操作最終肯定成功
        for (;;) {
            // 當前同步資源的個數 - 要獲取的同步資源個數,如果大於等於0則表示獲取同步資源成功,則通過CAS更新同步狀態的值,然後返回剩餘的可用資源個數,否則表示獲取同步資源失敗,返回負數
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }

    /**
     * 嘗試釋放同步資源
     */
    protected final boolean tryReleaseShared(int releases) {
        // 死迴圈保證操作最終肯定成功
        for (;;) {
            // 當前同步資源的個數 + 要釋放的同步資源個數 ,然後通過CAS更新同步狀態的值
            int current = getState();
            int next = current + releases;
            if (next < current) // 如果release是負數則丟擲異常
                throw new Error("Maximum permit count exceeded");
            if (compareAndSetState(current, next))
                return true;
        }
    }

    /**
     * 減少許可
     */
    final void reducePermits(int reductions) {
        // 死迴圈保證操作最終肯定成功
        for (;;) {
            // 當前同步資源的個數 - 要減少的同步資源個數,然後通過CAS更新同步狀態的值
            int current = getState();
            int next = current - reductions;
            if (next > current) // 如果reductions是負數,則丟擲異常
                throw new Error("Permit count underflow");
            if (compareAndSetState(current, next))
                return;
        }
    }

    /**
     * 耗盡許可
     */
    final int drainPermits() {
        // 死迴圈保證操作最終肯定成功
        for (;;) {
            // 通過CAS將同步狀態的值設定為0
            int current = getState();
            if (current == 0 || compareAndSetState(current, 0))
                return current;
        }
    }
}

可以看到抽象同步器的構建方法會初始化同步資源的個數,同時提供了nonfairTryAcquireShared()、tryReleaseShared()、reducePermits()、drainPermits()方法。

同時抽象同步器已經重寫了AQS的tryReleaseShared()方法,因此抽象同步器的子類還需要重寫AQS的tryAcquireShared()方法。


剖析非公平同步器

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -2694183684443567898L;

    /**
     * 顯式呼叫父類的帶permits引數的構建方法
     */
    NonfairSync(int permits) {
        super(permits);
    }

    /**
     * 嘗試獲取同步資源
     */
    protected int tryAcquireShared(int acquires) {
        // 直接呼叫抽象同步器的nonfairTryAcquireShared()方法
        return nonfairTryAcquireShared(acquires);
    }
}

可以看到非公平同步器的構造方法會顯式呼叫父類的帶permits引數的構建方法,用於初始化同步資源。

同時非公平同步器的tryAcquireShared()方法將會直接呼叫抽象同步器的nonfairTryAcquireShared()方法。


剖析公平同步器

static final class FairSync extends Sync {
    private static final long serialVersionUID = 2014338818796000944L;

    /**
     * 顯式呼叫父類的帶permits引數的構建方法
     */
    FairSync(int permits) {
        super(permits);
    }

    /**
     * 嘗試獲取同步資源
     */
    protected int tryAcquireShared(int acquires) {
        // 死迴圈保證操作最終肯定成功
        for (;;) {
            // 只有噹噹前執行緒就是等待佇列中頭節點的後繼節點所封裝的執行緒,或者當前等待佇列為空或只有一個節點,才允許嘗試獲取同步資源,否則表示獲取同步資源失敗,返回負數
            if (hasQueuedPredecessors())
                return -1;
            
            // 當前同步資源的個數 - 要獲取的同步資源個數,如果大於等於0則表示獲取同步資源成功,則通過CAS更新同步狀態的值,然後返回剩餘的可用資源個數,否則表示獲取同步資源失敗,返回負數
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }
}

可以看到公平同步器的構造方法將會顯示呼叫父類的帶permits引數的構造方法,用於初始化同步資源。

同時公平同步器重寫的tryAcquireShared()方法中,只有噹噹前執行緒是等待佇列中頭節點的後繼節點所封裝的執行緒,或者當前等待佇列為空或只有一個節點時,才允許嘗試獲取同步資源,否則獲取同步資源失敗,返回負數。


非公平模式下的總結

1.當執行緒要獲取許可時,可以直接呼叫Semaphore的acquire()和tryAcquire()方法。

2.如果呼叫acquire()方法,那麼將會直接呼叫AQS的acquireShared()方法,該方法又會呼叫非公平同步器的tryAcquireShared()方法,該方法會直接呼叫抽象同步器提供的nonfairTryAcquireShared()方法(用當前同步資源的個數 - 要獲取的同步資源個數,如果大於等於0則表示獲取同步資源成功,則通過CAS更新同步狀態的值,然後返回剩餘的可用資源個數,否則表示獲取同步資源失敗,返回負數)

3.如果呼叫tryAcquire()方法,那麼將會直接呼叫抽象同步器的提供的nonfairTryAcquireShared()方法嘗試獲取同步資源。

4.當執行緒要釋放許可時,將會呼叫Semaphore的release()方法,該方法會直接呼叫AQS的releaseShared()方法,releaseShared()方法又會呼叫抽象同步器的tryReleaseShared()方法,將當前同步資源的個數 + 要釋放的同步資源個數 ,然後通過CAS更新同步狀態的值。


公平模式下的總結

1.當執行緒要獲取許可時,可以直接呼叫Semaphore的acquire()和tryAcquire()方法。

2.如果呼叫acquire()方法,那麼將會直接呼叫AQS的acquireShared()方法,該方法又會呼叫公平同步器的tryAcquireShared()方法,該方法只有噹噹前執行緒是等待佇列中的頭節點的後繼節點所封裝的執行緒,或者當前等待佇列為空或只有一個節點時,才允許嘗試獲取同步資源(用當前同步資源的個數 - 要獲取的同步資源個數,如果大於等於0則表示獲取同步資源成功,則通過CAS更新同步狀態的值,然後返回剩餘的可用資源個數,否則表示獲取同步資源失敗,返回負數)

3.如果呼叫tryAcquire()方法,那麼將會直接呼叫抽象同步器的提供的nonfairTryAcquireShared()方法嘗試獲取同步資源。

4.當執行緒要釋放許可時,將會呼叫Semaphore的release()方法,該方法會直接呼叫AQS的releaseShared()方法,releaseShared()方法又會呼叫抽象同步器的tryReleaseShared()方法,將當前同步資源的個數 + 要釋放的同步資源個數 ,然後通過CAS更新同步狀態的值。


FAQ

非公平模式是如何保證非公平的?

主要體現在非公平同步器的tryAcquireShared()方法,直接用當前同步資源的個數 - 要獲取的同步資源個數,如果大於等於0則表示獲取同步資源成功,則通過CAS更新同步狀態的值,然後返回剩餘的可用資源個數,否則表示獲取同步資源失敗,返回負數。

當執行緒釋放同步資源時,需要喚醒離頭節點最近的同時等待狀態不為CANCELLED的後繼節點,然後在該節點嘗試獲取同步資源之前,如果其他執行緒直接呼叫了Semaphore的acquire()方法獲取許可,由於acquire()方法直接呼叫AQS的acquireShared()方法,該方法又會呼叫非公平同步器的tryAcquireShared()方法,所以執行緒會直接嘗試獲取同步資源,這就是非公平的體現。

公平模式是如何保證公平的?

主要體現在公平同步器的tryAcquireShared()方法,只有噹噹前執行緒是等待佇列中頭節點的後繼節點所封裝的執行緒,或當前等待佇列為空或只有一個節點時,才允許嘗試獲取同步資源,將當前同步資源的個數 - 要獲取的同步資源個數,如果大於等於0則表示獲取同步資源成功,則CAS更新同步狀態的值,然後返回剩餘的可用資源個數,否則表示獲取同步資源失敗,返回負數。

當執行緒釋放同步資源時,需要喚醒離頭節點最近的同時等待狀態不為CANCELLED的後繼節點,然後在該節點嘗試獲取同步資源之前,如果其他執行緒直接呼叫了Semaphore的acquire()方法獲取許可,由於acquire()方法直接呼叫AQS的acquireShared()方法,而該方法又呼叫公平同步器的tryAcquireShared()方法,由於當前執行緒並非等待佇列中頭節點的後繼節點所封裝的執行緒,因此該執行緒也只能封裝成Node節點,然後加入到等待佇列當中。

相關文章