你還在用Executors來創建線程池?會有什麼問題呢?

前言

我們知道,只要需要創建線程的情況下,即使是在單線程模式下,我們也要儘量使用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來創建線程池,那我們先來回顧一下Executors和ThreadPoolExecutor。

你還在用Executors來創建線程池?會有什麼問題呢?

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

ThreadPoolExecutor一共有四個構造函數,七個可配參數,分別是

corePoolSize: 線程池中保持存活線程的數量。maximumPoolSize: 線程池中允許線程數量的最大值keepAliveTime: 表示線程沒有任務執行時最多保持多久時間會終止unit: 參數keepAliveTime的時間單位workQueue: 一個阻塞隊列,用來存儲等待執行的任務threadFactory: 線程工廠,主要用來創建線程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>());
}
/<runnable>

可以看到對於存儲等待執行的任務,FixedThreadPool是通過LinkedBlockingQueue來實現的。而我們知道LinkedBlockingQueue是一個鏈表實現的阻塞隊列,而如果不設置其容量的話,將會是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE。

由於Executors中並未設置容量,所以應用可以不斷向隊列中添加任務,導致OOM錯誤

上面提到的問題主要體現在newFixedThreadPool和newSingleThreadExecutor兩個工廠方法上,並不是說newCachedThreadPool和newScheduledThreadPool這兩個方法就安全了,這兩種方式創建的最大線程數可能是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)); //添加容量大小
}
/<runnable>

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


分享到:


相關文章: