【Java系列003】揭祕阿里爲什麼禁止使用Executors建立線程池

你好,我是miniluo,不少面試官,老是喜歡問JDK線程池相關的問題,包括建立線程池的方式,線程池的參數等;做爲應聘者的你總會被這些問題所困擾。今天我就和你一塊兒學習Java線程池的相關知識,手動建立線程池java

咱們都知道JDK有四種建立線程池的方式,它們在不一樣的場景下都有其適用性。可是《阿里巴巴 Java 開發手冊》開發規範明確規定不能使用Executors建立線程池;我也有和你同樣的疑惑,接下來咱們就一塊兒分析分析。面試

JDK建立線程池最經常使用的四種方式分別是:緩存

一、newFixedThreadPool(int nThreads)建立固定數據線程dom

二、newCachedThreadPool()建立一個可緩存的線程池,調用execute將重用之前構成的線程。學習

三、newSingleThreadPoolExecutor()建立一個單線程的線程池google

四、newScheduledThreadPool(int corePoolSize)建立一個定時及週期性執行任務的線程池spa

從上面的描述來看咱們很容易知道各自適應不一樣場景,使用起來也很方便。咱們先來看newFixedThreadPool什麼狀況下會出現OOM。線程

爲了更快看到OOM,咱們先修改JVM最小和最大內存分別是-Xms128m -Xmx512m,以後咱們執行下面代碼日誌

@Slf4j
public class ExecutorsTest {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = (ThreadPoolExecutor)
                Executors.newFixedThreadPool(1);
        printThreadInfo(threadPool);
        for (int i = 0; i < 100000000; i++) {
            threadPool.execute(() -> {
                String randomValue = IntStream.rangeClosed(1, 1000000).mapToObj(test-> "c")
                        .collect(Collectors.joining(""))
                        + UUID.randomUUID().toString();
                try {
                    TimeUnit.HOURS.sleep(1);
                } catch (InterruptedException e) {
                }
                log.info(randomValue);
            });
        }
        threadPool.shutdown();
        try {
            threadPool.awaitTermination(1, TimeUnit.HOURS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void printThreadInfo(ThreadPoolExecutor threadPool) {
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
            log.info("=========================");
            log.info("Pool Size: {}", threadPool.getPoolSize());
            log.info("Active Threads: {}", threadPool.getActiveCount());
            log.info("Number of Tasks Completed: {}", threadPool.getCompletedTaskCount());
            log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
            log.info("=========================");
        }, 0, 1, TimeUnit.SECONDS);
    }
}}

讓代碼跑一會,喝口水回來咱們發現就出現如圖所示的異常。
 code

喝口水的工夫就出現OOM了,這要是擱生產那你電話就響了,那就是事故,得寫個事故報告了吧。

咱們翻看newFixedThreadPool方法的源碼發現,線程池的工做隊列直接new了一個LinkedBlockingQueue,而LinkedBlockingQueue默認的構造方法是一個長度爲Integer.MAX_VALUE的隊列,即無界隊列(直到耗盡全部JVM內存)。

newFixedThreadPool雖然固定線程數量,CPU沒有耗盡但是由於隊列的無界則可能會擠爆內存致使OOM。

咱們把建立線程池的方法修改成以下:

ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool();

讓程序跑一會,站起來扭下脖子再瞄下就出現OOM了,如圖所示。

 

再翻看下newCachedThreadPool的源碼,其默認的線程大小爲Integer.MAX_VALUE即無限制,可是其工做隊列SynchronousQueue,是一個無存儲空間的FIFO隊列。線程是須要分配必定的存儲空間做爲線程棧,案例中咱們模擬每一個線程處理時長爲1h,因此就會建立不少的線程,加入每一個1MB,則按咱們上文所設置的JVM內存大小,就垂手可得的發送了OOM(固然生產若存在佔用線程1h的狀況就應該拿出來分析了,這裏是爲了模擬OOM)。

至此,咱們對阿里爲什麼強制禁止使用JDK建立線程池的方式有了一些理解。

下面咱們來講說手動創線程池的好處,大概有如下幾點:

一、經過ThreadPoolExecutor的方式建立線程池,讓更加明確線程池的運行規則,規避資源耗盡的風險。

二、經過實現ThreadFactory,咱們能夠自定義有意義的線程名字,方便在出現問題的時候進行排查(特別是生產問題)。

三、經過實現RejectedExecutionHandler,咱們能夠作一些日誌記錄,有利於分析線程池的運行情況。

因爲手動建立線程池的代碼過多就不在這裏粘貼,有須要的能夠在留言處寫下郵箱,我逐一發送;後面我會把相關案例源碼上傳到GitHub供你們學習參考。

除了經過ThreadPoolExecutor建立線程池以外,還能經過commons-lang3和com.google.guava這兩個包提供的方法建立,有興趣的朋友能夠了解下。

思考和討論

一、或許你會以爲疑惑,那JDK提供的四種快捷建立線程池的方式目的是什麼?

二、另外兩種線程池所使用的工做隊列是什麼?

三、線程池核心最小核心線程數的設定規則有哪些?

歡迎留言與我分享和指正!也歡迎你把這篇文章分享給你的朋友或同事,一塊兒交流。

感謝您的閱讀,咱們下節再見!

掃碼關注咱們,與君共進

相關文章
相關標籤/搜索