線程池ThreadPoolExecutor使用注意事項

1. 線程池的作用:

  1. 重復利用已經(jīng)創(chuàng)建好的線程, 降低創(chuàng)建線程和銷毀線程的性能開銷
  2. 合理的設(shè)置線程池大小可以避免因為線程數(shù)超出硬件資源瓶頸帶來的問題,類似起到了限流的作用

2. 線程池的創(chuàng)建

2.1 原生線程池 ThreadPoolExecutor(推薦)
ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
image.png
2.2 JDK工具類 Executors(不推薦

1. newFixedThreadPool
工作線程控制在固定的數(shù)量上,但任務(wù)隊列是無界的

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

2 newCachedThreadPool
最大線程數(shù)無上限

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

3 使用注意事項

3.1 不要使用Executors提供的工具方法創(chuàng)建線程池,會導致OOM,原因已在上面分析
3.2 線程池參數(shù)的配置

1. corePoolSize、maximumPoolSize 的選擇
因為實際很難界定系統(tǒng)是IO密集型還是CPU密集型,并且tasks、taskcost也不是一成不變的,所以實際使用中先根據(jù)經(jīng)驗估算出一個經(jīng)驗值,然后再通過壓測驗證經(jīng)驗值, 這時如果能直觀的觀察到線程池的內(nèi)部狀態(tài)就非常有必要了

參數(shù) 備注
根據(jù)任務(wù)類型估算:
CPU密集型: N+1
IO密集型: corePoolSize 1, maximumPoolSize: 2N
《Java多線程變成實戰(zhàn)指南》
IO corePoolSize 為1, 是為了減少IO操作引起的上下文切換
根據(jù)tasks、taskcost、responsetime估算 https://www.lagou.com/lgeduarticle/18336.html
動態(tài)修改線程池配置 https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

1.2 BlockingQueue的選擇
通常認為LinkedBlockingQueue吞吐量會比ArrayBlockingQueueu高[http://www.itdecent.cn/p/5b85c1794351]
i. ArrayBlockingQueue: 存儲空間是預先分配的, 一把全局鎖
ii. LinkedBlockingQueue: 存取兩把鎖,用于存儲隊列元素的存儲空間是在其使用過程中動態(tài)分配的,因此它可能會增加JVM垃圾回收的負擔。
1.3 拒絕策略的選擇

圖片來源于《Java線程池實現(xiàn)原理及其在美團業(yè)務(wù)中的實踐》

ThreadPoolExecutor的擴展
因為ThreadPoolExecutor為了通用,只有在阻塞隊列滿的時候,才繼續(xù)創(chuàng)建線程,那有沒有辦法corePoolSize達到時,繼續(xù)創(chuàng)建線程,直到達到了maximumPoolSize才放到阻塞隊列中,可以參考唯品會開源的QueuableCachedThreadPool

3.3 線程池的監(jiān)控

線程池監(jiān)控主要為以下幾方面提供幫助:
1.為線程池調(diào)優(yōu)提供參考
1.幫助定位問題以及監(jiān)控報警

ThreadPoolExecutor提供一些方法暴露內(nèi)部狀態(tài),可以將指標記錄到監(jiān)控中

    private void doMetric() {
        // 當前線程池中運行的線程總數(shù)
        MetricClient.record("thread.pool.pool.size", threadPoolExecutor.getPoolSize());
        // 歷史峰值線程數(shù)
        MetricClient.record("thread.pool.largest.pool.size", threadPoolExecutor.getLargestPoolSize());
        // 當前任務(wù)隊列中積壓任務(wù)的總數(shù)
        MetricClient.record("thread.pool.queue.size", threadPoolExecutor.getQueue().size());
        // 當前活躍線程數(shù)
        MetricClient.record("thread.pool.active.size", threadPoolExecutor.getActiveCount());
    }
3.4 ThreadPoolExecutor需要在全局聲明

否則會重復創(chuàng)建多個線程池, 而且核心線程池不會被銷毀, 一個反例如下

    public static void main(String[] args) throws Exception {

        FixedThreadPool fixedThreadPool = new FixedThreadPool();

        for (int i = 0; i < 5; i++) {
            fixedThreadPool.test();
        }

        TimeUnit.SECONDS.sleep(100);
        System.out.println("Finished");
    }

    private void test() {
        ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
                1, 10,
                0, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(5),
                new ThreadFactoryBuilder().setNameFormat("threadPool-%d").build());

        threadPoolExecutor.submit(() -> System.out.println(Thread.currentThread().getName()));
    }

因為調(diào)用test函數(shù)5次,最終會有5個線程,且狀態(tài)為WAIT。


image.png
3.5 避免線程泄露

線程泄露是指線程池中的工作者意外終止,使得線程池中實際可用的工作者線程變少
如等待網(wǎng)絡(luò)I/O, 而該任務(wù)有沒有對這個等待指定時間限制,如果外部資源一直沒有返回該任務(wù)所等待的結(jié)果,就會導致該線程一直處于等待狀態(tài)而無法執(zhí)行其他任務(wù)。

3.6 線程池隔離
  1. 1 避免相互影響
    應(yīng)用中通常配置多個線程池,要根據(jù)任務(wù)的“輕重緩急”來指定線程池的核心參數(shù),包括線程數(shù)、回收策略和任務(wù)隊列,避免相互之間影響。
  2. 1 避免引發(fā)死鎖
    提交給同一線程池實例執(zhí)行的任務(wù)是相互獨立的,而不是彼此有依賴關(guān)系的任務(wù)
    如下是一個引起死鎖的極端例子, 任務(wù)A依賴任務(wù)B的執(zhí)行結(jié)果,等待B的執(zhí)行,任務(wù)B因為資源限制,位于阻塞隊列中,等待任務(wù)A執(zhí)行結(jié)束,造成死鎖
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 2, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5));

        Runnable taskA = () -> {
            System.out.println("Task A start.");
            Runnable taskB = () -> System.out.println("Process Task B");

            Future result = threadPoolExecutor.submit(taskB);

            try {
                result.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("Task A finished");
        };

        Future future = threadPoolExecutor.submit(taskA);

        future.get();

參考文獻:

  1. Java多線程編程實戰(zhàn)指南
  2. Java線程池實現(xiàn)原理及其在美團業(yè)務(wù)中的實踐
  3. ArrayBlockingQueue與LinkedBlockingQueue
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 為什么需要線程池 java中為了提高并發(fā)度,可以使用多線程共同執(zhí)行,但是如果有大量線程短時間之內(nèi)被創(chuàng)建和銷毀,會占...
    Java阿七閱讀 308評論 0 0
  • 項目中使用的線程池的地方很多,一直以來感覺對它的參數(shù)已經(jīng)掌握的很好了,但是遇到幾次問題之后才發(fā)現(xiàn)欠缺的這么多 遇到...
    十毛tenmao閱讀 7,256評論 0 17
  • 參考原文鏈接 在開發(fā)的過程中,如果并發(fā)的線程數(shù)量很多,并且每個線程都是執(zhí)行一個時間很短的任務(wù)就結(jié)束了,這樣頻繁創(chuàng)...
    WAHAHA402閱讀 278評論 0 0
  • 漸變的面目拼圖要我怎么拼? 我是疲乏了還是投降了? 不是不允許自己墜落, 我沒有滴水不進的保護膜。 就是害怕變得面...
    悶熱當乘涼閱讀 4,471評論 0 13
  • 感覺自己有點神經(jīng)衰弱,總是覺得手機響了;屋外有人走過;每次媽媽不聲不響的進房間突然跟我說話,我都會被嚇得半死!一整...
    章魚的擁抱閱讀 2,369評論 4 5

友情鏈接更多精彩內(nèi)容