分享一個生產者-消費者的真實場景

0.背景

現在有一個大數據平臺,我們需要通過spark對hive裏的數據讀取清洗轉換(etl)再加其它的業務操作的過程,然後需要把這批數據落地到tbase數據庫(騰訊的一款分佈式數據庫)。
數據導入的特點是不定時,但量大。每次導入的數據量在幾億到幾十億上百億之間。
如果使用dataset.write的方式寫入,spark內部也是使用的sql connection以jdbc的方式進行寫入。在這樣的數據量之下,會非常慢,慢到完全無法接受。

經研究,tbase底層爲pgsql,支持以文件的方式copy寫入。
語法爲:

COPY table FROM '/mnt/g/file.csv' WITH CSV HEADER;

這樣效率高了很多。

經過測試,十億級別的數據在半小時單位就能夠寫入。當然,建立了索引,以及隨着表數據量的增大,寫入效率會降低,但完全能夠接受。

那麼,現在就是使用spark讀取hive,經過處理,再dataset.repartion(num)重分區,將數據寫入HDFS形成num個文件。再將這些小文件多線程批量copy到tbds。

hdfs小文件數量nums從幾千到幾萬,而批量寫入的連接數connections不可能無限大,
把文件抽象成生產者,數據庫連接抽象成消費者。生產者源源不斷生產,消費者能力有限跟不上生產者的速率,就需要阻塞在消費端。

企業微信截圖_16811785837172.png

1.實現方式

生產者-消費者模式的實現,不論是自己使用鎖,還是使用阻塞隊列,其核心都是阻塞。

1.1 方式1 線程池自帶阻塞隊列

我們批量寫入是通過多線程來的,實現一個線程池的其中之一方法是通過Executors,並指定一個帶線程數的參數。
這樣的方式在線上7*24小時運行的業務系統中是絕對不推薦使用的,但在一些大數據平臺的定時任務也不是完全禁止,看自身情況。

使用Executors構建線程池最大問題在於它底層也是通過ThreadPoolExecutor來構建線程池,核心線程和最大線程相同,且阻塞隊列默認爲LinkedBlockingQueue,這個阻塞隊列
沒有設置長度,那麼它的最大長度爲Integer.MAX_VALUE
這樣就可能造成內存的無限增長,內存耗盡導致OOM。

企業微信截圖_16811788273157.png

企業微信截圖_16811788837994.png

但具體到我們現在的這個場景下,文件數爲幾千到幾萬,那麼線程池阻塞隊列的長度在這個範圍以內,如果平臺資源能夠接受,也不是不可以。
同時,剛好可以利用線程池的阻塞隊列來構建消費者-生產者。

public static void main(String[] args) throws Exception {
        List<File> fileList = cn.hutool.core.io.FileUtil.loopFiles(new File("測試路徑"));
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        LongAdder longAdder = new LongAdder();
        for(File file : fileList){
            try {
                executorService.execute(new TestRun(fileList, longAdder));
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        }
        executorService.shutdown();
    }

    public static class TestRun implements Runnable{
        private List<File> fileList;
        LongAdder longAdder;

        public TestRun(List<File> fileList, LongAdder longAdder) {
            this.fileList = fileList;
            this.longAdder = longAdder;
        }

        @SneakyThrows
        @Override
        public void run() {
            try {
                // 可通過連接池
                longAdder.increment();
                ConnectionUtils.getConnection();
                System.out.println(Thread.currentThread() + "第"+ longAdder.longValue() + "/"+ fileList.size() +"個文件獲取連接正在入庫");
                Random random = new Random();
                Thread.sleep(random.nextInt(1000));
                System.out.println(Thread.currentThread() + "第"+ longAdder.longValue() + "/"+ fileList.size() +"個文件完成入庫歸還連接");
            } finally {
            }
        }
    }

運行輸出:

數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
Thread[pool-1-thread-5,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-9,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-1,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-2,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-7,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-10,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-6,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-8,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-4,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-3,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-1,5,main]第10/33個文件完成入庫歸還連接
數據庫驅動加載成功
Thread[pool-1-thread-1,5,main]第11/33個文件獲取連接正在入庫
Thread[pool-1-thread-4,5,main]第11/33個文件完成入庫歸還連接
數據庫驅動加載成功
.
.
.
數據庫驅動加載成功
Thread[pool-1-thread-3,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-9,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-8,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-6,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-7,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-10,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-5,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-4,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-3,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-2,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-1,5,main]第33/33個文件完成入庫歸還連接


這裏的longAdder只是爲了方便觀看,並沒有嚴格按線程遞增。
我們模擬33個文件,線程池的核心大小爲10,可以看到最大隻有10個文件在同時執行,只有當其中文件入庫完畢,新的文件才能執行。達到了我們想要的效果。

1.2 方式2 使用阻塞隊列+CountDownLatch

CountDownLatch是什麼?

它是一種同步輔助工具,允許一個或多個線程等待,直到在其他線程中執行的一組操作完成。

CountDownLatch使用給定的計數進行初始化。await()會阻塞,直到當前計數由於countDown()的調用而達到零,之後所有等待線程都會被釋放,任何後續的await()調用都會立即返回。這是一種一次性現象——計數無法重置。

CountDownLatch是一種通用的同步工具,可用於多種目的。用計數1初始化的CountDownLatch用作簡單的開/關鎖存器或門:所有調用的線程都在門處等待,直到調用countDown的線程打開它。初始化爲N的CountDownLatch可以用來讓一個線程等待,直到N個線程完成了一些操作,或者一些操作已經完成了N次。

自定義一個阻塞隊列,並將這個阻塞隊列構建成數據庫連接池,使用10個固定的大小,只有文件take到連接纔會入庫操作,拿不到的時候就阻塞直到其它文件入庫完成歸還數據庫連接。

@Slf4j
public class ConnectionQueue {

    LinkedBlockingQueue<Connection> connections = null;

    private int size = 10;

    public ConnectionQueue(int size) throws Exception{
        new ConnectionQueue(null, size);
    }

    public ConnectionQueue(LinkedBlockingQueue<Connection> connections, int size) throws IllegalArgumentException{
        if (size <= 0 || size > 100) {
            throw new IllegalArgumentException("size 長度必須適宜,在1-100之間");
        }
        this.connections = connections;
        this.size = size;
    }

    /**
     * 初始化數據庫連接
     */
    public void init(){
        if (connections == null) {
            connections = new LinkedBlockingQueue<>(size);
        }
        for (int i = 0; i < size; i++) {
            connections.add(ConnectionUtils.getConnection());
        }
    }

    /**
     * 獲取一個數據庫連接,如果沒有空閒連接將阻塞直到拿到連接
     * @return
     * @throws InterruptedException
     */
    public Connection get() throws InterruptedException {
        return connections.take();
    }

    public Connection poll() throws InterruptedException {
        return connections.poll();
    }


    /**
     * 歸還空閒連接
     * @param connection
     */
    public void put(Connection connection){
        connections.add(connection);
    }

    public int size(){
        return connections.size();
    }

    /**
     * 銷燬
     */
    public void destroy() {
        Iterator<Connection> it = connections.iterator();
        while (it.hasNext()) {
            Connection conn = it.next();
            if (conn != null) {
                try {
                    conn.close();
                    log.info("關閉連接 " + conn);
                } catch (SQLException e) {
                    log.error("關閉連接失敗", e);
                }
            } else {
                log.info("conn = {}爲空", conn);
            }
        }
        if (connections != null) {
            connections.clear();
        }
    }
}

同時使用CountDownLatch進行計數,await()直到所有線程都執行完畢,再進行資源銷燬和其它業務操作。

public static void main(String[] args) throws Exception {
        List<File> fileList = cn.hutool.core.io.FileUtil.loopFiles(new File("測試路徑"));
        ConnectionQueue connectionQueue = new ConnectionQueue(10);
        connectionQueue.init();
        ExecutorService executorService = new ThreadPoolExecutor(10,
                10,
                1,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(10),
                 (r, executor) -> {
                     if (r instanceof Test.TestRun) {
                         ((TestRun) r).getCountDownLatch().countDown();
                     }
                     System.out.println(Thread.currentThread() +" reject countdown");
                 }
                );
        CountDownLatch countDownLatch = new CountDownLatch(fileList.size());
        for(File file : fileList){
            try {
                Connection conn = connectionQueue.get();
                executorService.execute(new TestRun(countDownLatch, connectionQueue, fileList, conn));
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        }

        countDownLatch.await();
        executorService.shutdown();
        connectionQueue.destroy();
    }

    public static class TestRun implements Runnable{
        private CountDownLatch countDownLatch;
        private ConnectionQueue connectionQueue;
        private Connection connection;
        private List<File> fileList;

        public TestRun(CountDownLatch countDownLatch, ConnectionQueue connectionQueue, List<File> fileList, Connection connection) {
            this.countDownLatch = countDownLatch;
            this.connectionQueue = connectionQueue;
            this.fileList = fileList;
            this.connection = connection;
        }

        public CountDownLatch getCountDownLatch() {
            return countDownLatch;
        }

        public void setCountDownLatch(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @SneakyThrows
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread() + "第"+ countDownLatch.getCount() + "/"+ fileList.size() +"個文件獲取連接正在入庫");
                Random random = new Random();
                Thread.sleep(random.nextInt(1000));
                System.out.println(Thread.currentThread() + "第"+ countDownLatch.getCount() + "/"+ fileList.size() +"個文件完成入庫歸還連接");
            } finally {
                connectionQueue.put(connection);
                countDownLatch.countDown();
            }
        }
    }

執行結果:

數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
數據庫驅動加載成功
Thread[pool-1-thread-1,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-4,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-3,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-2,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-10,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-6,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-7,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-8,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-9,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-5,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-4,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-4,5,main]第32/33個文件獲取連接正在入庫
Thread[pool-1-thread-8,5,main]第32/33個文件完成入庫歸還連接
Thread[pool-1-thread-8,5,main]第31/33個文件獲取連接正在入庫
Thread[pool-1-thread-8,5,main]第31/33個文件完成入庫歸還連接
Thread[pool-1-thread-8,5,main]第30/33個文件獲取連接正在入庫
Thread[pool-1-thread-4,5,main]第30/33個文件完成入庫歸還連接
...
Thread[pool-1-thread-2,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-5,5,main]第10/33個文件完成入庫歸還連接
Thread[pool-1-thread-4,5,main]第9/33個文件完成入庫歸還連接
Thread[pool-1-thread-9,5,main]第8/33個文件完成入庫歸還連接
Thread[pool-1-thread-2,5,main]第7/33個文件完成入庫歸還連接
Thread[pool-1-thread-6,5,main]第6/33個文件完成入庫歸還連接
Thread[pool-1-thread-7,5,main]第5/33個文件完成入庫歸還連接
Thread[pool-1-thread-10,5,main]第4/33個文件完成入庫歸還連接
Thread[pool-1-thread-3,5,main]第3/33個文件完成入庫歸還連接
Thread[pool-1-thread-1,5,main]第2/33個文件完成入庫歸還連接
Thread[pool-1-thread-8,5,main]第1/33個文件完成入庫歸還連接

1.2.1 如果線程池觸發reject會發生什麼?

需要注意的是,這裏要考慮到線程池的拒絕策略。

我們知道JDK線程池拒絕策略實現了四種:

AbortPolicy 默認策略,拋出異常
CallerRunsPolicy  從名字上可以看出,調用者執行
DiscardOldestPolicy 丟棄最老的任務,再嘗試執行
DiscardPolicy  直接丟棄不做任何操作

ThreadPoolExecutor默認拒絕策略爲AbortPolicy,就是拋出一個異常,那麼這時候就執行不到後面的countdown
所以需要重寫策略,在線程池隊列已滿拒絕新進任務的時候執行countdown,避免countDownLatch.await()永遠等待。

如果使用默認的拒絕策略,執行如下:

企業微信截圖_16811317416709.png

1.3 方式3 使用Semaphore

在 java 中,使用了 synchronized 關鍵字和 Lock 鎖實現了資源的併發訪問控制,在同一時刻只允許一個線程進入臨界區訪問資源 (讀鎖除外)。但考慮到另外一種場景,共享資源在同一時刻可以提供給多個線程訪問,如廁所有多個坑位,可以同時提供給多人使用。這種場景下,就可以使用Semaphore信號量來實現。

信號量通常用於限制可以訪問某些(物理或邏輯)資源的線程數量。信號量維護一組許可(permit),在訪問資源前,每個線程必須從信號量獲得一個許可,以保證資源的有限訪問。當線程處理完後,向信號量返回一個許可,允許另一個線程獲取。
當信號量許可>1,意味可以訪問資源,如果信號量許可<=0,線程進入休眠。
當信號量許可=1,約等於synchronizedlock的效果。

就好比一個廁所管理員,站在門口,只有廁所有空位,就開門允許與空側數量等量的人進入廁所。多個人進入廁所後,相當於N個人來分配使用N個空位。爲避免多個人來同時競爭同一個側衛,在內部仍然使用鎖來控制資源的同步訪問。

在我們的場景下,共享資源就是數據庫連接池N個,M個文件需要拿到連接池進行入庫操作,但連接池數量N有限,遠小於文件數M,所以需要對連接池的訪問併發度進行控制。

信號量在這裏起到了控流的作用。
Semaphore semaphore = new Semaphore(10);
允許線程池最多10個任務並行執行,只有當其它任務執行完畢歸還permit,新的任務拿到permit才能開始執行。

public static void main(String[] args) throws Exception {
        List<File> fileList = FileUtil.loopFiles(new File("測試路徑"));
        Semaphore semaphore = new Semaphore(10);

        Random random = new Random();
        ExecutorService executorService = new ThreadPoolExecutor(10,
                10,
                1,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(10));
        AtomicInteger count = new AtomicInteger(1);
        for (File file : fileList) {
            semaphore.acquire();
                executorService.execute(() -> {
                try {
                    int subCount = count.getAndIncrement();
                    System.out.println(Thread.currentThread() + "第" + subCount + "/" + fileList.size() + "個文件獲取連接正在入庫");
                    // 模擬入庫操作
                    int time = random.nextInt(1000);
                    Thread.sleep(time);
                    System.out.println(Thread.currentThread() + "第" + subCount + "/" + fileList.size() + "個文件完成入庫歸還連接");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            });
        }

        System.out.println("shutdown");
        executorService.shutdown();
    }

因爲我們的大數據框架本身有獲取連接池的輪子,這裏省略了從連接池獲取連接的操作。

運行日誌:

Thread[pool-1-thread-1,5,main]第1/33個文件獲取連接正在入庫
Thread[pool-1-thread-3,5,main]第3/33個文件獲取連接正在入庫
Thread[pool-1-thread-4,5,main]第2/33個文件獲取連接正在入庫
Thread[pool-1-thread-10,5,main]第5/33個文件獲取連接正在入庫
Thread[pool-1-thread-9,5,main]第4/33個文件獲取連接正在入庫
Thread[pool-1-thread-8,5,main]第8/33個文件獲取連接正在入庫
Thread[pool-1-thread-2,5,main]第9/33個文件獲取連接正在入庫
Thread[pool-1-thread-7,5,main]第7/33個文件獲取連接正在入庫
Thread[pool-1-thread-6,5,main]第6/33個文件獲取連接正在入庫
Thread[pool-1-thread-5,5,main]第10/33個文件獲取連接正在入庫
Thread[pool-1-thread-5,5,main]第10/33個文件完成入庫歸還連接
Thread[pool-1-thread-5,5,main]第11/33個文件獲取連接正在入庫
Thread[pool-1-thread-3,5,main]第3/33個文件完成入庫歸還連接
...
Thread[pool-1-thread-2,5,main]第23/33個文件完成入庫歸還連接
shutdown
Thread[pool-1-thread-2,5,main]第33/33個文件獲取連接正在入庫
Thread[pool-1-thread-4,5,main]第24/33個文件完成入庫歸還連接
Thread[pool-1-thread-5,5,main]第32/33個文件完成入庫歸還連接
Thread[pool-1-thread-1,5,main]第30/33個文件完成入庫歸還連接
Thread[pool-1-thread-9,5,main]第26/33個文件完成入庫歸還連接
Thread[pool-1-thread-3,5,main]第19/33個文件完成入庫歸還連接
Thread[pool-1-thread-2,5,main]第33/33個文件完成入庫歸還連接
Thread[pool-1-thread-8,5,main]第22/33個文件完成入庫歸還連接
Thread[pool-1-thread-6,5,main]第27/33個文件完成入庫歸還連接
Thread[pool-1-thread-10,5,main]第31/33個文件完成入庫歸還連接
Thread[pool-1-thread-7,5,main]第28/33個文件完成入庫歸還連接

1.3.1 如果引發了默認線程池拒絕策略,Semaphore會有問題嗎?

我們知道CountDownLatch由於線程池拒絕策略,沒有執行到countdown()會導致程序一直阻塞。那麼Semaphore會有相應的問題嗎?

如果線程池隊列滿了,觸發了默認拒絕策略,這時候,Semaphore執行了acquire(),但沒執行release()
寫一個測試例子:

public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(20);
        Semaphore semaphore = new Semaphore(10);
        ExecutorService executorService = new ThreadPoolExecutor(5,
                5,
                1,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(1), (r, executor) -> {
                    Random random = new Random();
                    try {
                        Thread.sleep(random.nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (r instanceof TestRun) {
                        ((TestRun) r).getCountDownLatch().countDown();
//                                    ((TestRun) r).getSemaphore().release();
                    }
                    System.out.println(Thread.currentThread() + " reject countdown " + semaphore.availablePermits());
        });


        for (int i = 0; i < 30; i++) {
            semaphore.acquire();
            Thread.sleep(100);
            executorService.execute(new TestRun(countDownLatch, semaphore));
        }

//        countDownLatch.await();
        System.out.println("完成");
        executorService.shutdown();
    }

    public static class TestRun implements Runnable {
        private CountDownLatch countDownLatch;
        private Semaphore semaphore;

        public TestRun(CountDownLatch countDownLatch, Semaphore semaphore) {
            this.countDownLatch = countDownLatch;
            this.semaphore = semaphore;
        }

        public CountDownLatch getCountDownLatch() {
            return countDownLatch;
        }

        public void setCountDownLatch(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        public Semaphore getSemaphore() {
            return semaphore;
        }

        public void setSemaphore(Semaphore semaphore) {
            this.semaphore = semaphore;
        }

        @SneakyThrows
        @Override
        public void run() {
//            semaphore.acquire();
            Random random = new Random();
            Thread.sleep(random.nextInt(1000));
            countDownLatch.countDown();
            semaphore.release();
            System.out.println(Thread.currentThread() + " start" + " semaphore = " + semaphore.availablePermits());
            System.out.println(Thread.currentThread() + " countdown");
        }
    }

執行日誌:

Thread[pool-1-thread-1,5,main] start semaphore = 8
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 5
Thread[pool-1-thread-3,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 4
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 5
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-5,5,main] start semaphore = 6
Thread[pool-1-thread-5,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 7
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 7
Thread[pool-1-thread-4,5,main] start semaphore = 5
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 5
Thread[pool-1-thread-3,5,main] countdown
Thread[pool-1-thread-4,5,main] start semaphore = 4
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-5,5,main] start semaphore = 3
Thread[pool-1-thread-5,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 3
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 4
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 4
Thread[pool-1-thread-4,5,main] start semaphore = 4
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 4
Thread[pool-1-thread-3,5,main] countdown
Thread[pool-1-thread-5,5,main] start semaphore = 3
Thread[pool-1-thread-5,5,main] countdown
Thread[pool-1-thread-4,5,main] start semaphore = 3
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 2
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 2
Thread[pool-1-thread-3,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 2
Thread[pool-1-thread-3,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 3
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-4,5,main] start semaphore = 4
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-5,5,main] start semaphore = 5
Thread[pool-1-thread-5,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 6
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 6
完成
Thread[pool-1-thread-5,5,main] start semaphore = 4
Thread[pool-1-thread-5,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 5
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-4,5,main] start semaphore = 6
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 7
Thread[pool-1-thread-3,5,main] countdown

可以看到執行了3次reject,最後semaphore值爲7,正常應該爲初始值10。
首先程序能夠正常執行完畢,然後併發度下降了。
如果極端情況下,觸發拒絕策略增多,semaphore的值降爲1,這裏semaphore就變成了lock或者synchronized,多線程就失去了效果變成了單線程串行執行。

通過JDK線程池拒絕策略之一的CallerRunsPolicy源碼可知,這裏的r即爲調用者線程,在這裏就是main線程。我們在main線程執行了acquire(),那麼我們只需要重寫拒絕策略,在這裏執行release()就可保證併發度與初始值保持一致。

企業微信截圖_16811326841516.png

但是如果semaphore=0呢?會阻塞執行嗎?

1.3.2 如果初始化的時候就爲0

Semaphore semaphore = new Semaphore(0);

那麼程序會永遠阻塞不執行,因爲沒有可用的permit。

jdk源碼這裏沒有對傳入的參數做判斷,甚至可以傳入負數。

因爲與countdownlatch不同,這裏可以釋放增加任意大於0的permit數量。

1.3.3 如果reject次數大於等於初始化長度

初化長度大於1,比如10,
Semaphore semaphore = new Semaphore(10);
同時,線程池拒絕次數>= 10,理論上,這個時候Semaphore就會出現0或負數。
線程就會阻塞。

但這種情況真的會發生嗎?

我模擬了很多次都沒出現阻塞的情況。
把線程池大小調整爲1,將Semaphore大小設置爲>1,這裏爲4。

public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(20);
        Semaphore semaphore = new Semaphore(4);
        ExecutorService executorService = new ThreadPoolExecutor(1,
                1,
                1,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(1), (r, executor) -> {
                    Random random = new Random();
                    try {
                        Thread.sleep(random.nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (r instanceof TestRun) {
                        ((TestRun) r).getCountDownLatch().countDown();
        //                            ((TestRun) r).getSemaphore().acquire();
//                                    ((TestRun) r).getSemaphore().release();
                    }
                    System.out.println(Thread.currentThread() + " reject countdown " + semaphore.availablePermits());
        });


        for (int i = 0; i < 30; i++) {
            semaphore.acquire();
//            Thread.sleep(100);
            executorService.execute(new TestRun(countDownLatch, semaphore));
        }

//        countDownLatch.await();
        System.out.println("完成");
        executorService.shutdown();
    }

    public static class TestRun implements Runnable {
        private CountDownLatch countDownLatch;
        private Semaphore semaphore;

        public TestRun(CountDownLatch countDownLatch, Semaphore semaphore) {
            this.countDownLatch = countDownLatch;
            this.semaphore = semaphore;
        }

        public CountDownLatch getCountDownLatch() {
            return countDownLatch;
        }

        public void setCountDownLatch(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        public Semaphore getSemaphore() {
            return semaphore;
        }

        public void setSemaphore(Semaphore semaphore) {
            this.semaphore = semaphore;
        }

        @SneakyThrows
        @Override
        public void run() {
//            semaphore.acquire();
            Random random = new Random();
            Thread.sleep(random.nextInt(1000));
            countDownLatch.countDown();
            semaphore.release();
            System.out.println(Thread.currentThread() + " start" + " semaphore = " + semaphore.availablePermits());
            System.out.println(Thread.currentThread() + " countdown");
        }
    }

執行結果:

Thread[pool-1-thread-1,5,main] start semaphore = 2
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 2
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 1
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 0
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 0
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
完成
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown

最後semaphore = 1.
當我將semaphore初始化值調整爲3,5,2,最後semaphore的值總是爲1。
線程池觸發拒絕次數總是爲semaphore初始化值-1

其實也很好理解,因爲當permit>=1的時候,acquire()方法纔會返回,不然就一直阻塞。所以初始permit>0的情況下,永遠不會出現permit爲0。


所以,結論是隻要semaphore的初始值大於0,就不用擔心程序會一直阻塞不執行。
同時,線程池觸發拒絕策略,如果沒有重寫拒絕策略執行semaphore.release(),就會將併發度降低。

2. 總結

1.直接使用線程池隊列要注意阻塞隊列大小爲Integer.MAX_VALUE可能導致內存消耗問題。
2.這裏使用信號量最爲簡單便捷。
3.不管使用的是coundownlatch還是信號量,都要注意線程池拒絕的情況。
如果countdownlatch因爲線程池拒絕策略沒有執行countdown會導致await一直等待阻塞;
如果信號量因爲線程池拒絕策略沒有執行release,導致沒有足夠的permit,不會導致程序阻塞,但會降低併發 度。

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