# 為什么禁止使用 Executors 創建線程池?
在《深入源碼分析 Java 線程池的實現原理》這篇文章中,我們介紹過了 Java中線程池的常見用法以及基本原理。
在文中有這樣一段描述:
可以通過 Executors 靜態工廠構建線程池,但一般不建議這樣使用。
關于這個問題,在那篇文章中并沒有深入的展開。作者之所以這么說,是因為這種創建線程池的方式有很大的隱患,稍有不慎就有可能導致線上故障,如:一次
Java 線程池誤用引發的血案和總結。
本文我們就來圍繞這個問題來分析一下為什么 JDK 自身提供的構建線程池的方式并不建議使用?到底應該如何創建一個線程池呢?
## Executors
Executors 是一個 Java 中的工具類。提供工廠方法來創建不同類型的線程池。

從上圖中也可以看出,Executors 的創建線程池的方法,創建出來的線程池都實現了 ExecutorService 接口。常用方法有以下幾個:
newFiexedThreadPool(int Threads):創建固定數目線程的線程池。
newCachedThreadPool():創建一個可緩存的線程池,調用 execute 將重用
以前構造的線程(如果線程可用)。如果沒有可用的線程,則創建一個新線程并添加到池中。終止并從緩存中移除那些已有 60 秒鐘未被使用的線程。
newSingleThreadExecutor() 創建一個單線程化的 Executor。
newScheduledThreadPool(int corePoolSize) 創建一個支持定時及周
期性的任務執行的線程池,多數情況下可用來替代 Timer 類。
類看起來功能還是比較強大的,又用到了工廠模式、又有比較強的擴展性,重要的是用起來還比較方便,如:
ExecutorService executor = Executors.newFixedThreadPool(nThreads) ;
即可創建一個固定大小的線程池。
但是為什么我說不建議大家使用這個類來創建線程池呢?
我提到的是『不建議』,但是在 Java 開發手冊中也明確指出,而且用的詞是
『不允許』使用 Executors 創建線程池。

## Executors 存在什么問題
在 Java 開發手冊中提到,使用 Executors 創建線程池可能會導致 OOM(Out-OfMemory , 內存溢出 ),但是并沒有說明為什么,那么接下來我們就來看一下到底為什么不允許使用 Executors ?
我們先來一個簡單的例子,模擬一下使用 Executors 導致 OOM 的情況。
```
/**
* @author Hollis
*/
public class ExecutorsDemo {
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 java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.
java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.
java:1371)
at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)
```
以上代碼指出,ExecutorsDemo.java 的第 16 行,就是代碼中的 execu-
tor.execute(new SubThread());。
## Executors 為什么存在缺陷
通過上面的例子,我們知道了 Executors 創建的線程池存在 OOM 的風險,那么到底是什么原因導致的呢?我們需要深入 Executors 的源碼來分析一下。
其實,在上面的報錯信息中,我們是可以看出蛛絲馬跡的,在以上的代碼中其實已經說了,真正的導致 OOM 的其實是 LinkedBlockingQueue.offer 方法。
```
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.
java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.
java:1371)
at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)
```
如果讀者翻看代碼的話,也可以發現,其實底層確實是通過 LinkedBlock-
ingQueue 實現的:
```
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
```
如果讀者對 Java 中的阻塞隊列有所了解的話,看到這里或許就能夠明白原
因了。
Java 中的 BlockingQueue 主要有兩種實現,分別是 ArrayBlockingQ-
ueue 和 LinkedBlockingQueue。
ArrayBlockingQueue 是一個用數組實現的有界阻塞隊列,必須設置容量。
LinkedBlockingQueue 是一個用鏈表實現的有界阻塞隊列,容量可以選擇
進行設置,不設置的話,將是一個無邊界的阻塞隊列,最大長度為 Integer.MAX_VALUE。
這里的問題就出在:不設置的話,將是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE。也就是說,如果我們不設置 LinkedBlockingQueue 的容量的話,其默認容量將會是 Integer.MAX_VALUE。
而 newFixedThreadPool 中創建 LinkedBlockingQueue 時,并未指定容
量。此時,LinkedBlockingQueue 就是一個無邊界隊列,對于一個無邊界隊列來說,是可以不斷的向隊列中加入任務的,這種情況下就有可能因為任務過多而導致內存溢出問題。
上面提到的問題主要體現在 newFixedThreadPool 和 newSingleThrea-
dExecutor 兩個工廠方法上,并不是說 newCachedThreadPool 和newSched-
uledThreadPool 這兩個方法就安全了,這兩種方式創建的最大線程數可能是
Integer.MAX_VALUE,而創建這么多線程,必然就有可能導致 OOM。
## 創建線程池的正確姿勢
避免使用 Executors 創建線程池,主要是避免使用其中的默認實現,那么我們可以自己直接調用 ThreadPoolExecutor 的構造函數來自己創建線程池。在創建的同時,給 BlockQueue 指定容量就可以了。
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
這種情況下,一旦提交的線程數超過當前可用線程數時,就會拋出 java.util.
concurrent.RejectedExecutionException,這是因為當前線程池使用的隊列
是有邊界隊列,隊列已經滿了便無法繼續處理新的請求。但是異常(Exception)總比發生錯誤(Error)要好。除了自己定義 ThreadPoolExecutor 外。還有其他方法。這個時候第一時間
就應該想到開源類庫,如 apache 和 guava 等。
作者推薦使用 guava 提供的 ThreadFactoryBuilder 來創建線程池。
```
public class ExecutorsDemo {
private static ThreadFactory namedThreadFactory = new
ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d").build();
private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new
ThreadPoolExecutor.
AbortPolicy());
public static void main(String[] args) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
pool.execute(new SubThread());
}
}
}
```
通過上述方式創建線程時,不僅可以避免 OOM 的問題,還可以自定義線程名稱,更加方便的出錯的時候溯源。
思考題,文中作者說:發生異常(Exception)要比發生錯誤(Error)好,為什么這么說?
- java開發手冊
- 嵩山版手冊
- 為什么禁止使用 Apache Beanutils 進行屬性的 copy ?
- 為什么阿里巴巴要求日期格式化時必須有使用y表示年,而不能用Y?
- 《Java 開發手冊 - 泰山版》提到的三目運算符 的空指針問題到底是個怎么回事?
- 為什么建議初始化 HashMap 的容量大小?
- Java 開發手冊建議創建 HashMap 時設置初始 化容量,但是多少合適呢?
- 為什么禁止使用 Executors 創建線程池?
- 為什么要求謹慎使用 ArrayList 中的 subList 方法?
- 為什么不建議在 for 循環中使用“+”進行字符 串拼接?
- 為什么禁止在 foreach 循環里進行元素的 remove/add 操作?
- 為什么禁止工程師直接使用日志系統 (Log4j、 Logback) 中的 API ?
- 為什么禁止把 SimpleDateFormat 定義成 static 變量?
- 為什么禁止開發人員使用 isSuccess 作為變量名?
- 為什么禁止開發人員修改 serialVersionUID 字段的值?
- 為什么建議開發者謹慎使用繼承?
- 為什么禁止使用 count( 列名 ) 或 count( 常量 ) 來替代 count(*) ?