Java中线程池,你真的会用吗
轉(zhuǎn)載自? ?Java中線程池,你真的會用嗎
在《深入源碼分析Java線程池的實現(xiàn)原理》這篇文章中,我們介紹過了Java中線程池的常見用法以及基本原理。
在文中有這樣一段描述:
可以通過Executors靜態(tài)工廠構(gòu)建線程池,但一般不建議這樣使用。
關(guān)于這個問題,在那篇文章中并沒有深入的展開。作者之所以這么說,是因為這種創(chuàng)建線程池的方式有很大的隱患,稍有不慎就有可能導(dǎo)致線上故障。
本文我們就來圍繞這個問題來分析一下為什么JDK自身提供的構(gòu)建線程池的方式并不建議使用?到底應(yīng)該如何創(chuàng)建一個線程池呢?
?
Executors
Executors 是一個Java中的工具類。提供工廠方法來創(chuàng)建不同類型的線程池。
從上圖中也可以看出,Executors的創(chuàng)建線程池的方法,創(chuàng)建出來的線程池都實現(xiàn)了ExecutorService接口。常用方法有以下幾個:
newFiexedThreadPool(int Threads):創(chuàng)建固定數(shù)目線程的線程池。
newCachedThreadPool():創(chuàng)建一個可緩存的線程池,調(diào)用execute 將重用以前構(gòu)造的線程(如果線程可用)。如果沒有可用的線程,則創(chuàng)建一個新線程并添加到池中。終止并從緩存中移除那些已有 60 秒鐘未被使用的線程。
newSingleThreadExecutor()創(chuàng)建一個單線程化的Executor。
newScheduledThreadPool(int corePoolSize)創(chuàng)建一個支持定時及周期性的任務(wù)執(zhí)行的線程池,多數(shù)情況下可用來替代Timer類。
類看起來功能還是比較強大的,又用到了工廠模式、又有比較強的擴展性,重要的是用起來還比較方便,如:
ExecutorService executor = Executors.newFixedThreadPool(nThreads) ;即可創(chuàng)建一個固定大小的線程池。
但是為什么我說不建議大家使用這個類來創(chuàng)建線程池呢?
我提到的是『不建議』,但是在阿里巴巴Java開發(fā)手冊中也明確指出,而且用的詞是『不允許』使用Executors創(chuàng)建線程池。
?
?
Executors存在什么問題
在阿里巴巴Java開發(fā)手冊中提到,使用Executors創(chuàng)建線程池可能會導(dǎo)致OOM(OutOfMemory ,內(nèi)存溢出),但是并沒有說明為什么,那么接下來我們就來看一下到底為什么不允許使用Executors?
我們先來一個簡單的例子,模擬一下使用Executors導(dǎo)致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?{@Overridepublic?void?run()?{try?{Thread.sleep(10000);}?catch?(InterruptedException e) {//do nothing}} }通過指定JVM參數(shù):-Xmx8m -Xms8m?運行以上代碼,會拋出OOM:
Exception?in?thread?"main"?java.lang.OutOfMemoryError:?GC?overhead?limit?exceededat?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行,就是代碼中的executor.execute(new SubThread());。
?
?
Executors為什么存在缺陷
通過上面的例子,我們知道了Executors創(chuàng)建的線程池存在OOM的風(fēng)險,那么到底是什么原因?qū)е碌哪?#xff1f;我們需要深入Executors的源碼來分析一下。
其實,在上面的報錯信息中,我們是可以看出蛛絲馬跡的,在以上的代碼中其實已經(jīng)說了,真正的導(dǎo)致OOM的其實是LinkedBlockingQueue.offer方法。
Exception?in?thread?"main"?java.lang.OutOfMemoryError:?GC?overhead?limit?exceededat?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)如果讀者翻看代碼的話,也可以發(fā)現(xiàn),其實底層確實是通過LinkedBlockingQueue實現(xiàn)的:
public?static?ExecutorService?newFixedThreadPool(int?nThreads)?{return?new?ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new?LinkedBlockingQueue<Runnable>());如果讀者對Java中的阻塞隊列有所了解的話,看到這里或許就能夠明白原因了。
Java中的BlockingQueue主要有兩種實現(xiàn),分別是ArrayBlockingQueue?和?LinkedBlockingQueue。
ArrayBlockingQueue是一個用數(shù)組實現(xiàn)的有界阻塞隊列,必須設(shè)置容量。
LinkedBlockingQueue是一個用鏈表實現(xiàn)的有界阻塞隊列,容量可以選擇進行設(shè)置,不設(shè)置的話,將是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE。
這里的問題就出在:不設(shè)置的話,將是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE。也就是說,如果我們不設(shè)置LinkedBlockingQueue的容量的話,其默認容量將會是Integer.MAX_VALUE。
而newFixedThreadPool中創(chuàng)建LinkedBlockingQueue時,并未指定容量。此時,LinkedBlockingQueue就是一個無邊界隊列,對于一個無邊界隊列來說,是可以不斷的向隊列中加入任務(wù)的,這種情況下就有可能因為任務(wù)過多而導(dǎo)致內(nèi)存溢出問題。
上面提到的問題主要體現(xiàn)在newFixedThreadPool和newSingleThreadExecutor兩個工廠方法上,并不是說newCachedThreadPool和newScheduledThreadPool這兩個方法就安全了,這兩種方式創(chuàng)建的最大線程數(shù)可能是Integer.MAX_VALUE,而創(chuàng)建這么多線程,必然就有可能導(dǎo)致OOM。
?
?
創(chuàng)建線程池的正確姿勢
避免使用Executors創(chuàng)建線程池,主要是避免使用其中的默認實現(xiàn),那么我們可以自己直接調(diào)用ThreadPoolExecutor的構(gòu)造函數(shù)來自己創(chuàng)建線程池。在創(chuàng)建的同時,給BlockQueue指定容量就可以了。
private?static?ExecutorService executor =?new?ThreadPoolExecutor(10,?10,60L, TimeUnit.SECONDS,new?ArrayBlockingQueue(10));這種情況下,一旦提交的線程數(shù)超過當前可用線程數(shù)時,就會拋出java.util.concurrent.RejectedExecutionException,這是因為當前線程池使用的隊列是有邊界隊列,隊列已經(jīng)滿了便無法繼續(xù)處理新的請求。但是異常(Exception)總比發(fā)生錯誤(Error)要好。
除了自己定義ThreadPoolExecutor外。還有其他方法。這個時候第一時間就應(yīng)該想到開源類庫,如apache和guava等。
作者推薦使用guava提供的ThreadFactoryBuilder來創(chuàng)建線程池。
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());}} }通過上述方式創(chuàng)建線程時,不僅可以避免OOM的問題,還可以自定義線程名稱,更加方便的出錯的時候溯源。
思考題,文中作者說:發(fā)生異常(Exception)要比發(fā)生錯誤(Error)好,為什么這么說?
總結(jié)
以上是生活随笔為你收集整理的Java中线程池,你真的会用吗的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何在2022年取消你的Shopify账
- 下一篇: Java中如何实现线程的超时中断