線程池的創建分爲兩種方式:ThreadPoolExecutor 和 Executors,上一節學習了 ThreadPoolExecutor 的使用方式,本節重點來看 Executors 是如何創建線程池的。
Executors 可以創建以下六種線程池。
-
FixedThreadPool(n):創建一個數量固定的線程池,超出的任務會在隊列中等待空閒的線程,可用於控制程序的最大併發數。
-
CachedThreadPool():短時間內處理大量工作的線程池,會根據任務數量產生對應的線程,並試圖緩存線程以便重複使用,如果限制 60 秒沒被使用,則會被移除緩存。
-
SingleThreadExecutor():創建一個單線程線程池。
-
ScheduledThreadPool(n):創建一個數量固定的線程池,支持執行定時性或週期性任務。
-
SingleThreadScheduledExecutor():此線程池就是單線程的 newScheduledThreadPool。
-
WorkStealingPool(n):Java 8 新增創建線程池的方法,創建時如果不設置任何參數,則以當前機器處理器個數作爲線程個數,此線程池會並行處理任務,不能保證執行順序。
下面分別來看以上六種線程池的具體代碼使用。
FixedThreadPool 使用
創建固定個數的線程池,具體示例如下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
fixedThreadPool.execute(() -> {
System.out.println("CurrentTime - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
以上程序執行結果如下:
CurrentTime - 2019-06-27 20:58:58
CurrentTime - 2019-06-27 20:58:58
CurrentTime - 2019-06-27 20:58:59
根據執行結果可以看出,newFixedThreadPool(2) 確實是創建了兩個線程,在執行了一輪(2 次)之後,停了一秒,有了空閒線程,才執行第三次。
CachedThreadPool 使用
根據實際需要自動創建帶緩存功能的線程池,具體代碼如下:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
cachedThreadPool.execute(() -> {
System.out.println("CurrentTime - " +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
以上程序執行結果如下:
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
CurrentTime - 2019-06-27 21:24:46
根據執行結果可以看出,newCachedThreadPool 在短時間內會創建多個線程來處理對應的任務,並試圖把它們進行緩存以便重複使用。
SingleThreadExecutor 使用
創建單個線程的線程池,具體代碼如下:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 3; i++) {
singleThreadExecutor.execute(() -> {
System.out.println("CurrentTime - " +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
以上程序執行結果如下:
CurrentTime - 2019-06-27 21:43:34
CurrentTime - 2019-06-27 21:43:35
CurrentTime - 2019-06-27 21:43:36
ScheduledThreadPool 使用
創建一個可以執行週期性任務的線程池,具體代碼如下:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
scheduledThreadPool.schedule(() -> {
System.out.println("ThreadPool:" + LocalDateTime.now());
}, 1L, TimeUnit.SECONDS);
System.out.println("CurrentTime:" + LocalDateTime.now());
以上程序執行結果如下:
CurrentTime:2019-06-27T21:54:21.881
ThreadPool:2019-06-27T21:54:22.845
根據執行結果可以看出,我們設置的 1 秒後執行的任務生效了。
SingleThreadScheduledExecutor 使用
創建一個可以執行週期性任務的單線程池,具體代碼如下:
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
singleThreadScheduledExecutor.schedule(() -> {
System.out.println("ThreadPool:" + LocalDateTime.now());
}, 1L, TimeUnit.SECONDS);
System.out.println("CurrentTime:" + LocalDateTime.now());
WorkStealingPool 使用
Java 8 新增的創建線程池的方式,可根據當前電腦 CPU 處理器數量生成相應個數的線程池,使用代碼如下:
ExecutorService workStealingPool = Executors.newWorkStealingPool();
for (int i = 0; i < 5; i++) {
int finalNumber = i;
workStealingPool.execute(() -> {
System.out.println("I:" + finalNumber);
});
}
Thread.sleep(5000);
以上程序執行結果如下:
I:0
I:3
I:2
I:1
I:4
根據執行結果可以看出,newWorkStealingPool 是並行處理任務的,並不能保證執行順序。
ThreadPoolExecutor VS Executors
ThreadPoolExecutor 和 Executors 都是用來創建線程池的,其中 ThreadPoolExecutor 創建線程池的方式相對傳統,而 Executors 提供了更多的線程池類型(6 種),但很不幸的消息是在實際開發中並不推薦使用 Executors 的方式來創建線程池。
無獨有偶《阿里巴巴 Java 開發手冊》中對於線程池的創建也是這樣規定的,內容如下:
線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的讀者更加明確線程池的運行規則,規避資源耗盡的風險。
說明:Executors 返回的線程池對象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。
OOM 是 OutOfMemoryError 的縮寫,指內存溢出的意思。
爲什麼不允許使用 Executors?
我們先來看一個簡單的例子:
ExecutorService maxFixedThreadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
maxFixedThreadPool.execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
之後設置 JVM(Java 虛擬機)的啓動參數: -Xmx10m -Xms10m
(設置 JVM 最大運行內存等於 10M)運行程序,會拋出 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 xxx.main(xxx.java:127)
爲什麼 Executors 會存在 OOM 的缺陷?
通過以上代碼,找到了 FixedThreadPool 的源碼,代碼如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可以看到創建 FixedThreadPool 使用了 LinkedBlockingQueue 作爲任務隊列,繼續查看 LinkedBlockingQueue 的源碼就會發現問題的根源,源碼如下:
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
當使用 LinkedBlockingQueue 並沒有給它指定長度的時候,默認長度爲 Integer.MAX_VALUE,這樣就會導致程序會給線程池隊列添加超多個任務,因爲任務量太大就有造成 OOM 的風險。
相關面試題
1.以下程序會輸出什麼結果?
public static void main(String[] args) {
ExecutorService workStealingPool = Executors.newWorkStealingPool();
for (int i = 0; i < 5; i++) {
int finalNumber = i;
workStealingPool.execute(() -> {
System.out.print(finalNumber);
});
}
}
A:不輸出任何結果
B:輸出 0 到 9 有序數字
C:輸出 0 到 9 無需數字
D:以上全對
答:A
題目解析:newWorkStealingPool 內部實現是 ForkJoinPool,它會隨着主程序的退出而退出,因爲主程序沒有任何休眠和等待操作,程序會一閃而過,不會執行任何信息,所以也就不會輸出任何結果。
2.Executors 能創建單線程的線程池嗎?怎麼創建?
答:Executors 可以創建單線程線程池,創建分爲兩種方式:
-
Executors.newSingleThreadExecutor():創建一個單線程線程池。
-
Executors.newSingleThreadScheduledExecutor():創建一個可以執行週期性任務的單線程池。
3.Executors 中哪個線程適合執行短時間內大量任務?
答:newCachedThreadPool() 適合處理大量短時間工作任務。它會試圖緩存線程並重用,如果沒有緩存任務就會新創建任務,如果線程的限制時間超過六十秒,則會被移除線程池,因此它比較適合短時間內處理大量任務。
4.可以執行週期性任務的線程池都有哪些?
答:可執行週期性任務的線程池有兩個,分別是:newScheduledThreadPool() 和 newSingleThreadScheduledExecutor(),其中 newSingleThreadScheduledExecutor() 是 newScheduledThreadPool() 的單線程版本。
5.JDK 8 新增了什麼線程池?有什麼特點?
答:JDK 8 新增的線程池是 newWorkStealingPool(n),如果不指定併發數(也就是不指定 n),newWorkStealingPool() 會根據當前 CPU 處理器數量生成相應個數的線程池。它的特點是並行處理任務的,不能保證任務的執行順序。
6.newFixedThreadPool 和 ThreadPoolExecutor 有什麼關係?
答:newFixedThreadPool 是 ThreadPoolExecutor 包裝,newFixedThreadPool 底層也是通過 ThreadPoolExecutor 實現的。
newFixedThreadPool 的實現源碼如下:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
7.單線程的線程池存在的意義是什麼?
答:單線程線程池提供了隊列功能,如果有多個任務會排隊執行,可以保證任務執行的順序性。單線程線程池也可以重複利用已有線程,減低系統創建和銷燬線程的性能開銷。
8.線程池爲什麼建議使用 ThreadPoolExecutor 創建,而非 Executors?
答:使用 ThreadPoolExecutor 能讓開發者更加明確線程池的運行規則,避免資源耗盡的風險。
Executors 返回線程池的缺點如下:
-
FixedThreadPool 和 SingleThreadPool 允許請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量請求,可能會導致內存溢出;
-
CachedThreadPool 和 ScheduledThreadPool 允許創建線程數量爲 Integer.MAX_VALUE,創建大量線程,可能會導致內存溢出。
總結
Executors 可以創建 6 種不同類型的線程池,其中 newFixedThreadPool() 適合執行單位時間內固定的任務數,newCachedThreadPool() 適合短時間內處理大量任務,newSingleThreadExecutor() 和 newSingleThreadScheduledExecutor() 爲單線程線程池,而 newSingleThreadScheduledExecutor() 可以執行週期性的任務,是 newScheduledThreadPool(n) 的單線程版本,而 newWorkStealingPool() 爲 JDK 8 新增的併發線程池,可以根據當前電腦的 CPU 處理數量生成對比數量的線程池,但它的執行爲併發執行不能保證任務的執行順序。
下一篇:ThreadLocal詳解
在公衆號菜單中可自行獲取專屬架構視頻資料,包括不限於 java架構、python系列、人工智能系列、架構系列,以及最新面試、小程序、大前端均無私奉獻,你會感謝我的哈
往期精選