前言 "上文" 我們介紹了JDK中的線程池框架 。我們知道,只要需要創建線程的情況下,即使是在單線程模式下,我們也要儘量使用 。即: 但是,在 "《阿裡巴巴Java開發手冊》" 中有一條 【強制】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這 ...
前言
上文我們介紹了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
來創建線程池,那我們先來回顧一下Executors
和ThreadPoolExecutor
。
我們可以看到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>());
}
可以看到對於存儲等待執行的任務,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)); //添加容量大小
}
除了自己定義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
您的點贊與支持是作者持續更新的最大動力!