【Java併發工具】Semaphore

目錄

1 前言

2 應用場景

3 其它方法

4 實現原理

4.1 實現的同步器

4.2 執行過程


1 前言

本人使用jdk8版本。

Semaphore(信號量)是用來控制同時訪問特定資源的線程數量,它通過協調各個線程,以保證合理的使用公共資源。

舉個例子,比如××馬路要限制流量,只允許同時有一百輛車在這條路上行使,其他的都必須在路口等待,所以前一百輛車會看到綠燈,可以開進這條馬路,後面的車會看到紅燈,不能駛入××馬路,但是如果前一百輛中有5輛車已經離開了××馬路,那麼後面就允許有5輛車駛入馬路,這個例子裏說的車就是線程,駛入馬路就表示線程在執行,離開馬路就表示線程執行完成,看見紅燈就表示線程被阻塞,不能執行。

2 應用場景

Semaphore可以用於做流量控制,特別是公用資源有限的應用場景,比如數據庫連接。

假如有一個需求,要從本地磁盤讀取幾萬個文件的數據,因爲都是IO密集型任務,我們可以啓動幾十個線程併發地讀取,但是如果讀到內存後,還需要存儲到數據庫中(必須通過數據庫連接來寫),而數據庫的連接數只有10個,這時我們必須控制只有10個線程同時獲取數據庫連接保存數據,否則會報錯無法獲取數據庫連接。

public class SemaphoreTest {
    private static final int THREAD_COUNT = 30;
    private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
    private static Semaphore s = new Semaphore(10);

    public static void main(String[] args) {
        for (int i = 0; i < THREAD_COUNT; i++) {
            threadPool.execute(new MyRunnable());
        }
        threadPool.shutdown();
    } 
}

    class MyRunnable implements Runnable {

        @Override
        public void run() {
            try {
                s.acquire();
                System.out.println("save data -- ";
                s.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

在代碼中,雖然有30個線程在執行,但是隻允許10個併發執行。Semaphore的構造方法Semaphore(int permits)接受一個整型的數字,表示可用的許可證數量。Semaphore(10)表示允許10個線程獲取許可證,也就是最大併發數是10

Semaphore的用法也很簡單,首先線程使用Semaphoreacquire()方法獲取一個許可證,使用完之後調用release()方法歸還許可證。還可以用tryAcquire()方法嘗試獲取許可證。

3 其它方法

  • int availablePermits():返回此信號量中當前可用的許可證數。
  • int getQueueLength():返回正在等待獲取許可證的線程數。
  • boolean hasQueuedThreads():是否有線程正在等待獲取許可證。
  • void reducePermits(int reduction):減少 reduction個許可證,是個protected方法。
  • Collection<Thread> getQueuedThreads():返回所有等待獲取許可證的線程集合,是個protected方法。

4 實現原理

Semaphore內部實現了一個AbstractQueuedSynchronizer的子類,並重寫它的tryAcquireShared()、tryReleaseShared()等方法來實現“控制同時訪問特定資源的線程數量”的功能。顯然,實現基於同步器的共享鎖,在構造Semaphore時傳入的int參數就是用來指定共享鎖可以被獲取的最大數,超過最大數後再獲取線程會阻塞,同時一旦一個線程釋放自己的共享鎖,阻塞隊列中所有的等待線程會被喚醒來競爭鎖。多個線程怎麼來競爭鎖呢?Semaphore裏也引入同步器裏的公平鎖和非公平鎖的機制,在構造Semaphore時也可以指定。

4.1 實現的同步器

公平鎖與非公平鎖只在獲取鎖時有區別,釋放時用的是統一方法。

    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 (;;) {
                int available = getState();
                int remaining = available - acquires;        
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

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

        // 嘗試釋放鎖,沒有公平與非公平之分
        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;
            }
        }
    }

4.2 執行過程

通過上面的數據庫例子來介紹,構造Semaphore時將鎖的狀態設爲10,然後同時開啓30個線程往數據庫中寫,所有線程都會先調用Semaphore.acquire(),前10個線程會成功獲取鎖,每當獲取成功時鎖的狀態會-1,進而執行保存數據的操作,最終鎖的狀態爲0。當而另外20個線程再獲取鎖會失敗進而阻塞,若此前10個線程中有線程保存完數據釋放鎖了,鎖的狀態+1,會喚醒這個20個線程去競爭鎖(公平與非公平)。

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