【搞定面試官】你還在用Executors來建立執行緒池?會有什麼問題呢?

店小不二發表於2019-12-03

前言

上文我們介紹了JDK中的執行緒池框架Executor。我們知道,只要需要建立執行緒的情況下,即使是在單執行緒模式下,我們也要儘量使用Executor。即:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1); 
//此處不該利用Executors工具類來初始化執行緒池複製程式碼

但是,在《阿里巴巴Java開發手冊》中有一條

【強制】執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。

Executors 返回的執行緒池物件的弊端如下:

FixedThreadPool 和 SingleThreadPool : 允許的請求佇列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。

CachedThreadPool 和 ScheduledThreadPool : 允許的建立執行緒數量為 Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致 OOM。

可以看到,這是一個強制性的規則,並且是不允許使用Executors來建立,建議使用ThreadPoolExecutor來建立執行緒池,那我們先來回顧一下ExecutorsThreadPoolExecutor

我們可以看到ThreadPoolExecutor已經是Executor的具體實現了,而且具有較多可配引數(可配引數見下方,可僅瞭解,用到時再進行詳細查詢)。Executors是一個建立執行緒池的工具類,檢視其原始碼的話也會發現這幾種建立執行緒池的方法也都是通過呼叫ThreadPoolExecutor來實現的。

ThreadPoolExecutor一共有四個建構函式,七個可配引數,分別是

1. corePoolSize: 執行緒池中保持存活執行緒的數量。

2. maximumPoolSize: 執行緒池中允許執行緒數量的最大值

3. keepAliveTime: 表示執行緒沒有任務執行時最多保持多久時間會終止

4. unit: 引數keepAliveTime的時間單位

5. workQueue: 一個阻塞佇列,用來儲存等待執行的任務

6. threadFactory: 執行緒工廠,主要用來建立執行緒

7. handler:表示當拒絕處理任務時的策略

分析

那麼Executors到底會導致什麼問題,才會讓開發手冊中直接被定義為不允許了呢。首先就是一個血淋淋的教訓,直接導致線上服務不可用,已經可以算是事故了。

實驗

我們也可以現在我們本地進行一下小實驗:

public class ExecutorsTesting {
    private static ExecutorService executor = Executors.newFixedThreadPool(15);
    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executor.execute(new SubThread());
        }
    }
}

class SubThread implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            //do nothing
        }
    }
}複製程式碼

執行時指定JVM引數:-Xmx8m -Xms8m,大概幾秒鐘之後,會報出OOM錯誤:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at com.kaikeba.mybatis.ExecutorsTesting.main(ExecutorsTesting.java:10) 
    //報錯行數為上述程式碼中的executor.execute(new SubThread());複製程式碼

那麼為什麼會報出這個錯誤呢。

原始碼分析

我們先來看一下Executors中的FixedThreadPool是如何構造的。

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

可以看到對於儲存等待執行的任務,FixedThreadPool是通過LinkedBlockingQueue來實現的。而我們知道LinkedBlockingQueue是一個連結串列實現的阻塞佇列,而如果不設定其容量的話,將會是一個無邊界的阻塞佇列,最大長度為Integer.MAX_VALUE由於Executors中並未設定容量,所以應用可以不斷向佇列中新增任務,導致OOM錯誤

上面提到的問題主要體現在newFixedThreadPoolnewSingleThreadExecutor兩個工廠方法上,並不是說newCachedThreadPoolnewScheduledThreadPool這兩個方法就安全了,這兩種方式建立的最大執行緒數可能是Integer.MAX_VALUE,而建立這麼多執行緒,必然就有可能導致OOM。

如何該利用ThreadPoolExecutor來建立執行緒池呢?

我們其實可以看到Executors中的newFixedThreadPool其實也是呼叫ThreadPoolExecutor來實現的。正如手冊中所說,當我們不用Executors預設建立執行緒池的方法,而直接自己手動去呼叫ThreadPoolExecutor,可以讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。比如我們在Executors.newFixedThreadPool基礎上給LinkedBlockingQueue加一個容量,當佇列已經滿了,而仍需要新增新的請求會丟擲相應異常,我們可以根據異常做相應處理。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(10)); //新增容量大小
}複製程式碼

除了自己定義ThreadPoolExecutor外。還可以利用其它開源類庫,如apache和guava等,可以有更多個性化配置。

參考文章:

https://www.hollischuang.com/archives/2888

https://stackoverflow.com/questions/1094867/when-should-we-use-javas-thread-over-executor#answer-34373289

https://blog.51cto.com/zero01/2306857

本文由部落格一文多發平臺 OpenWrite 釋出!

文章首發:https://zhuanlan.zhihu.com/lovebell

個人公眾號:技術Go

您的點贊與支援是作者持續更新的最大動力!

相關文章