深讀源碼-java同步系列之Semaphore源碼解析

問題

(1)Semaphore是什麼?

(2)Semaphore具有哪些特性?

(3)Semaphore通常使用在什麼場景中?

(4)Semaphore的許可次數是否可以動態增減?

(5)Semaphore如何實現限流?

簡介

Semaphore,信號量,它保存了一系列的許可(permits),每次調用acquire()都將消耗一個許可,每次調用release()都將歸還一個許可。

特性

Semaphore通常用於限制同一時間對共享資源的訪問次數(訪問的線程個數),也就是常說的限流。

下面我們一起來學習Java中Semaphore是如何實現的。

類結構

Semaphore

Semaphore中包含了一個實現了AQS的同步器Sync,以及它的兩個子類FairSync和NonFairSync,這說明Semaphore也是區分公平模式和非公平模式的。

源碼分析

基於之前對於ReentrantLock和ReentrantReadWriteLock的分析,這篇文章相對來說比較簡單,之前講過的一些方法將直接略過,有興趣的可以拉到文章底部查看之前的文章。

內部類Sync

// java.util.concurrent.Semaphore.Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 1192457210091910933L;
    // 構造方法,傳入許可次數,放入state中
    Sync(int permits) {
        setState(permits);
    }
    // 獲取許可次數
    final int getPermits() {
        return getState();
    }
    // 非公平模式嘗試獲取許可
    final int nonfairTryAcquireShared(int acquires) {
        for (;;) {
            // 看看還有幾個許可
            int available = getState();
            // 減去這次需要獲取的許可還剩下幾個許可
            int remaining = available - acquires;
            // 如果剩餘許可小於0了則直接返回
            // 如果剩餘許可不小於0,則嘗試原子更新state的值,成功了返回剩餘許可
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }
    // 釋放許可
    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;
        }
    }
    // 減少許可
    final void reducePermits(int reductions) {
        for (;;) {
            // 看看還有幾個許可
            int current = getState();
            // 減去將要減少的許可
            int next = current - reductions;
            // 檢測舉出
            if (next > current) // underflow
                throw new Error("Permit count underflow");
            // 原子更新state的值,成功了返回true
            if (compareAndSetState(current, next))
                return;
        }
    }
    // 銷燬許可
    final int drainPermits() {
        for (;;) {
            // 看看還有幾個許可
            int current = getState();
            // 如果爲0,直接返回
            // 如果不爲0,把state原子更新爲0
            if (current == 0 || compareAndSetState(current, 0))
                return current;
        }
    }
}

通過Sync的幾個實現方法,我們獲取到以下幾點信息:

(1)許可是在構造方法時傳入的;

(2)許可存放在狀態變量state中;

(3)嘗試獲取一個許可的時候,則state的值減1;

(4)當state的值爲0的時候,則無法再獲取許可;

(5)釋放一個許可的時候,則state的值加1;

(6)許可的個數可以動態改變;

內部類NonfairSync

// java.util.concurrent.Semaphore.NonfairSync
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -2694183684443567898L;
    // 構造方法,調用父類的構造方法
    NonfairSync(int permits) {
        super(permits);
    }
    // 嘗試獲取許可,調用父類的nonfairTryAcquireShared()方法
    protected int tryAcquireShared(int acquires) {
        return nonfairTryAcquireShared(acquires);
    }
}

非公平模式下,直接調用父類的nonfairTryAcquireShared()嘗試獲取許可。

內部類FairSync

// java.util.concurrent.Semaphore.FairSync
static final class FairSync extends Sync {
    private static final long serialVersionUID = 2014338818796000944L;
    // 構造方法,調用父類的構造方法
    FairSync(int permits) {
        super(permits);
    }
    // 嘗試獲取許可
    protected int tryAcquireShared(int acquires) {
        for (;;) {
            // 公平模式需要檢測是否前面有排隊的
            // 如果有排隊的直接返回失敗
            if (hasQueuedPredecessors())
                return -1;
            // 沒有排隊的再嘗試更新state的值
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }
}

公平模式下,先檢測前面是否有排隊的,如果有排隊的則獲取許可失敗,進入隊列排隊,否則嘗試原子更新state的值。

構造方法

// 構造方法,創建時要傳入許可次數,默認使用非公平模式
public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
// 構造方法,需要傳入許可次數,及是否公平模式
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

創建Semaphore時需要傳入許可次數。

Semaphore默認也是非公平模式,但是你可以調用第二個構造方法聲明其爲公平模式。

下面的方法在學習過前面的內容看來都比較簡單,彤哥這裏只列舉Semaphore支持的一些功能了。

以下的方法都是針對非公平模式來描述。

acquire()方法

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

獲取一個許可,默認使用的是可中斷方式,如果嘗試獲取許可失敗,會進入AQS的隊列中排隊。

acquireUninterruptibly()方法

public void acquireUninterruptibly() {
    sync.acquireShared(1);
}

獲取一個許可,非中斷方式,如果嘗試獲取許可失敗,會進入AQS的隊列中排隊。

tryAcquire()方法

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

嘗試獲取一個許可,使用Sync的非公平模式嘗試獲取許可方法,不論是否獲取到許可都返回,只嘗試一次,不會進入隊列排隊。

tryAcquire(long timeout, TimeUnit unit)方法

public boolean tryAcquire(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

嘗試獲取一個許可,先嚐試一次獲取許可,如果失敗則會等待timeout時間,這段時間內都沒有獲取到許可,則返回false,否則返回true;

release()方法

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

釋放一個許可,釋放一個許可時state的值會加1,並且會喚醒下一個等待獲取許可的線程。

acquire(int permits)方法

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

一次獲取多個許可,可中斷方式。

acquireUninterruptibly(int permits)方法

public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireShared(permits);
}

一次獲取多個許可,非中斷方式。

tryAcquire(int permits)方法

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

一次嘗試獲取多個許可,只嘗試一次。

tryAcquire(int permits, long timeout, TimeUnit unit)方法

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

嘗試獲取多個許可,並會等待timeout時間,這段時間沒獲取到許可則返回false,否則返回true。

release(int permits)方法

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

一次釋放多個許可,state的值會相應增加permits的數量。

availablePermits()方法

public int availablePermits() {
    return sync.getPermits();
}

獲取可用的許可次數。

drainPermits()方法

public int drainPermits() {
    return sync.drainPermits();
}

銷燬當前可用的許可次數,對於已經獲取的許可沒有影響,會把當前剩餘的許可全部銷燬。

reducePermits(int reduction)方法

protected void reducePermits(int reduction) {
    if (reduction < 0) throw new IllegalArgumentException();
    sync.reducePermits(reduction);
}

減少許可的次數。

下面通過一個例子來看一下Semaphore的具體使用:

  假若一個工廠有5臺機器,但是有8個工人,一臺機器同時只能被一個工人使用,只有使用完了,其他工人才能繼續使用。那麼我們就可以通過Semaphore來實現:

import java.util.concurrent.Semaphore;
import java.util.stream.IntStream;

/**
 * @ClassName SemaphoreTest
 * @Author suidd
 * @Description Semaphore使用示例
 * 假若一個工廠有5臺機器,但是有8個工人,一臺機器同時只能被一個工人使用,只有使用完了,其他工人才能繼續使用。那麼我們就可以通過Semaphore來實現
 * @Date 15:02 2020/5/24
 * @Version 1.0
 **/
public class SemaphoreTest2 {
    public static void main(String[] args) {
        int workerNum = 8;// 工人數
        int machineCount = 5;// 機器數
        Semaphore semaphore = new Semaphore(machineCount);
        IntStream.range(1, workerNum + 1).
                forEach(i -> new Work(i, semaphore).start());
    }

    static class Work extends Thread {
        private int workerNo;
        private Semaphore semaphore;
        
        public Work(int workerNo, Semaphore semaphore) {
            this.workerNo = workerNo;
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                // 獲取許可
                semaphore.acquire();
                System.out.println("工人" + this.workerNo + "佔用一臺機器,正在生產中...");
                Thread.sleep(2000);
                System.out.println("工人" + this.workerNo + "生產完成,釋放出機器...");
                // 釋放許可
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

總結

(1)Semaphore,也叫信號量,通常用於控制同一時刻對共享資源的訪問次數(訪問的線程個數),也就是限流場景;

(2)Semaphore的內部實現是基於AQS的共享鎖來實現的;

(3)Semaphore初始化的時候需要指定許可的次數,許可的次數是存儲在state中;

(4)獲取一個許可時,則state值減1;

(5)釋放一個許可時,則state值加1;

(6)可以動態減少n個許可;

(7)可以動態增加n個許可;

彩蛋

(1)如何動態增加n個許可?

答:調用release(int permits)即可。我們知道釋放許可的時候state的值會相應增加,再回頭看看釋放許可的源碼,發現與ReentrantLock的釋放鎖還是有點區別的,Semaphore釋放許可的時候並不會檢查當前線程有沒有獲取過許可,所以可以調用釋放許可的方法動態增加一些許可。

(2)如何實現限流?

答:限流,即在流量突然增大的時候,上層要能夠限制住突然的大流量對下游服務的衝擊,在分佈式系統中限流一般做在網關層,當然在個別功能中也可以自己簡單地來限流,比如秒殺場景,假如只有10個商品需要秒殺,那麼,服務本身可以限制同時只進來100個請求,其它請求全部作廢,這樣服務的壓力也不會太大。

使用Semaphore就可以直接針對這個功能來限流,以下是代碼實現:

import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

/**
 * @ClassName SemaphoreTest
 * @Author suidd
 * @Description 使用Semaphore實現秒殺限流功能
 * @Date 15:38 2020/5/24
 * @Version 1.0
 **/
public class SemaphoreTest {
    public static final Semaphore SEMAPHORE = new Semaphore(100);
    public static final AtomicInteger failCount = new AtomicInteger(0);
    public static final AtomicInteger successCount = new AtomicInteger(0);

    public static void main(String[] args) {
        IntStream.range(0, 1000).
                forEach(i -> new Thread(() -> seckill()).start());
    }

     /**
      * @Author suidd
      * @Description 秒殺
      * @Date 15:46 2020/5/24
      * @Param
      * @return
      * @Version 1.0
      **/
    public static boolean seckill() {
        if (!SEMAPHORE.tryAcquire()) {
            System.out.println("no permits, count=" + failCount.incrementAndGet());
            return false;
        }

        try {
            // 處理業務邏輯
            Thread.sleep(2000);
            System.out.println("seckill success, count=" + successCount.incrementAndGet());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            SEMAPHORE.release();
        }
        return true;
    }
}

參考鏈接:https://www.cnblogs.com/tong-yuan/p/Semaphore.html

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