为什么创建线程池一定要用ThreadPoolExecutor?
作者 | 磊哥
來源 | Java面試真題解析(ID:aimianshi666)
轉(zhuǎn)載請聯(lián)系授權(quán)(微信ID:GG_Stone)
在 Java 語言中,并發(fā)編程都是依靠線程池完成的,而線程池的創(chuàng)建方式又有很多,但從大的分類來說,線程池的創(chuàng)建總共分為兩大類:手動方式使用 ThreadPoolExecutor 創(chuàng)建線程池和使用 Executors 執(zhí)行器自動創(chuàng)建線程池。那究竟要使用哪種方式來創(chuàng)建線程池呢?我們今天就來詳細(xì)的聊一聊。
先說結(jié)論
在 Java 語言中,一定要使用 ThreadPoolExecutor 手動的方式來創(chuàng)建線程池,因?yàn)檫@種方式可以通過參數(shù)來控制最大任務(wù)數(shù)和拒絕策略,讓線程池的執(zhí)行更加透明和可控,并且可以規(guī)避資源耗盡的風(fēng)險(xiǎn)。
OOM風(fēng)險(xiǎn)演示
假如我們使用了 Executors 執(zhí)行器自動創(chuàng)建線程池的方式來創(chuàng)建線程池,那么就會存現(xiàn)線程溢出的風(fēng)險(xiǎn),以 CachedThreadPool 為例我們來演示一下:
import?java.util.ArrayList; import?java.util.List; import?java.util.concurrent.ExecutorService; import?java.util.concurrent.Executors;public?class?ThreadPoolExecutorExample?{static?class?OOMClass?{//?創(chuàng)建?1MB?大小的變量(1M?=?1024KB?=?1024*1024Byte)private?byte[]?data_byte?=?new?byte[1?*?1024?*?1024];}public?static?void?main(String[]?args)?throws?InterruptedException?{//?使用執(zhí)行器自動創(chuàng)建線程池ExecutorService?threadPool?=?Executors.newCachedThreadPool();List<Object>?list?=?new?ArrayList<>();//?添加任務(wù)for?(int?i?=?0;?i?<?10;?i++)?{int?finalI?=?i;threadPool.execute(new?Runnable()?{@Overridepublic?void?run()?{//?定時添加try?{Thread.sleep(finalI?*?200);}?catch?(InterruptedException?e)?{e.printStackTrace();}//?將?1M?對象添加到集合OOMClass?oomClass?=?new?OOMClass();list.add(oomClass);System.out.println("執(zhí)行任務(wù):"?+?finalI);}});}} }第 2 步將 Idea 中 JVM 最大運(yùn)行內(nèi)存設(shè)置為 10M(設(shè)置此值主要是為了方便演示),如下圖所示:以上程序的執(zhí)行結(jié)果如下圖所示:從上述結(jié)果可以看出,當(dāng)線程執(zhí)行了 7 次之后就開始出現(xiàn) OutOfMemoryError 內(nèi)存溢出的異常了。
內(nèi)存溢出原因分析
想要了解內(nèi)存溢出的原因,我們需要查看 CachedThreadPool 實(shí)現(xiàn)的細(xì)節(jié),它的源碼如下圖所示:構(gòu)造函數(shù)的第 2 個參數(shù)被設(shè)置成了 Integer.MAX_VALUE,這個參數(shù)的含義是最大線程數(shù),所以由于 CachedThreadPool 并不限制線程的數(shù)量,當(dāng)任務(wù)數(shù)量特別多的時候,就會創(chuàng)建非常多的線程。而上面的 OOM 示例,每個線程至少要消耗 1M 大小的內(nèi)存,加上 JDK 系統(tǒng)類的加載也要占用一部分的內(nèi)存,所以當(dāng)總的運(yùn)行內(nèi)存大于 10M 的時候,就出現(xiàn)內(nèi)存溢出的問題了。
使用ThreadPoolExecutor來改進(jìn)
接下來我們使用 ThreadPoolExecutor 來改進(jìn)一下 OOM 的問題,我們使用 ThreadPoolExecutor 手動創(chuàng)建線程池的方式,創(chuàng)建一個最大線程數(shù)為 2,最多可存儲 2 個任務(wù)的線程池,并且設(shè)置線程池的拒絕策略為忽略新任務(wù),這樣就能保證線程池的運(yùn)行內(nèi)存大小不會超過 10M 了,實(shí)現(xiàn)代碼如下:
import?java.util.ArrayList; import?java.util.List; import?java.util.concurrent.*;/***?ThreadPoolExecutor?演示示例*/ public?class?ThreadPoolExecutorExample?{static?class?OOMClass?{//?創(chuàng)建?1MB?大小的變量(1M?=?1024KB?=?1024*1024Byte)private?byte[]?data_byte?=?new?byte[1?*?1024?*?1024];}public?static?void?main(String[]?args)?throws?InterruptedException?{//?手動創(chuàng)建線程池,最大線程數(shù)?2,最多存儲?2?個任務(wù),其他任務(wù)會被忽略ThreadPoolExecutor?threadPool?=?new?ThreadPoolExecutor(2,?2,0L,?TimeUnit.SECONDS,?new?LinkedBlockingQueue<>(2),new?ThreadPoolExecutor.DiscardPolicy());?//?拒絕策略:忽略任務(wù)List<Object>?list?=?new?ArrayList<>();//?添加任務(wù)for?(int?i?=?0;?i?<?10;?i++)?{int?finalI?=?i;threadPool.execute(new?Runnable()?{@Overridepublic?void?run()?{//?定時添加try?{Thread.sleep(finalI?*?200);}?catch?(InterruptedException?e)?{e.printStackTrace();}//?將?1m?對象添加到集合OOMClass?oomClass?=?new?OOMClass();list.add(oomClass);System.out.println("執(zhí)行任務(wù):"?+?finalI);}});}//?關(guān)閉線程池threadPool.shutdown();//?檢測線程池的任務(wù)執(zhí)行完while?(!threadPool.awaitTermination(3,?TimeUnit.SECONDS))?{System.out.println("線程池中還有任務(wù)在處理");}} }以上程序的執(zhí)行結(jié)果如下圖所示:從上述結(jié)果可以看出,線程池從開始執(zhí)行到執(zhí)行結(jié)束都沒有出現(xiàn) OOM 的異常,這就是手動創(chuàng)建線程池的優(yōu)勢。
其他創(chuàng)建線程池的問題
除了 CachedThreadPool 線程池之外,其他使用 Executors 自動創(chuàng)建線程池的方式,也存在著其他一些問題,比如 FixedThreadPool 它的實(shí)現(xiàn)源碼如下:而默認(rèn)情況下任務(wù)隊(duì)列 LinkedBlockingQueue 的存儲容量是 Integer.MAX_VALUE,也是趨向于無限大,如下圖所示:這樣就也會造成,因?yàn)榫€程池的任務(wù)過多而導(dǎo)致的內(nèi)存溢出問題。其他幾個使用 Executors 自動創(chuàng)建線程池的方式也存在此問題,這里就不一一演示了。
總結(jié)
線程池的創(chuàng)建方式總共分為兩大類:手動使用 ThreadPoolExecutor 創(chuàng)建線程池和自動使用 Executors 執(zhí)行器創(chuàng)建線程池的方式。其中使用 Executors 自動創(chuàng)建線程的方式,因?yàn)榫€程個數(shù)或者任務(wù)個數(shù)不可控,可能會導(dǎo)致內(nèi)存溢出的風(fēng)險(xiǎn),所以在創(chuàng)建線程池時,建議使用 ThreadPoolExecutor 的方式來創(chuàng)建。
是非審之于己,毀譽(yù)聽之于人,得失安之于數(shù)。
公眾號:Java面試真題解析
面試合集:https://gitee.com/mydb/interview
往期推薦面試突擊31:什么是守護(hù)線程?它和用戶線程有什么區(qū)別?
面試突擊30:線程池是如何執(zhí)行的?拒絕策略有哪些?
面試突擊29:說一下線程池7個參數(shù)的含義?
總結(jié)
以上是生活随笔為你收集整理的为什么创建线程池一定要用ThreadPoolExecutor?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于 MyBatis 手撸一个分表插件
- 下一篇: 数组中的filter方法_数组filte