由於不知道Java執行緒池的bug,某程式設計師叕被祭天

說說你對執行緒池的理解?

首先明確,池化的意義在於

快取,建立效能開銷較大的物件

,比如執行緒池、連線池、記憶體池。預先在池裡建立一些物件,使用時直接取,用完就歸還複用,使用策略調整池中快取物件的數量。

Java建立物件,僅是在JVM堆分塊記憶體,但建立一個執行緒,卻需呼叫os核心API,然後os要為執行緒分配一系列資源,成本很高,所以執行緒是一個重量級物件,應避免頻繁建立或銷燬。既然這麼麻煩,就要避免呀,所以要使用執行緒池!

一般池化資源,當你需要資源時,就呼叫申請執行緒方法申請資源,用完後呼叫釋放執行緒方法釋放資源。但JDK的執行緒池根本沒有申請執行緒和釋放執行緒的方法。

那到底該如何理解它的設計思想呢?其實執行緒池的設計,採用的是

生產者-消費者模式

執行緒池的使用方是生產者

執行緒池本身是消費者

以下簡化程式碼即可顯示執行緒池的基本原理:

由於不知道Java執行緒池的bug,某程式設計師叕被祭天

JDK執行緒池最核心的就是ThreadPoolExecutor,看名字,它強調的是Executor,並非一般的池化資源。

為什麼都說要手動宣告執行緒池?

雖然JDK的Executors工具類提供的方法可快速建立執行緒池。

由於不知道Java執行緒池的bug,某程式設計師叕被祭天

但阿里有話說:

由於不知道Java執行緒池的bug,某程式設計師叕被祭天

弊端真的這麼嚴重嗎,newFixedThreadPool=OOM?

寫段測試程式碼:

由於不知道Java執行緒池的bug,某程式設計師叕被祭天

執行不久,出現OOM

Exception in thread “http-nio-30666-ClientPoller”java。lang。OutOfMemoryError: GC overhead limit exceeded

newFixedThreadPool執行緒池的工作佇列直接new了一個LinkedBlockingQueue

但其預設構造器是一個Integer。MAX_VALUE長度的佇列,所以很快Q滿

雖然使用newFixedThreadPool可以固定工作執行緒數量,但任務佇列幾乎無界。若任務較多且執行較慢,佇列就會快速積壓,記憶體不夠,易導致OOM。

newCachedThreadPool也等於OOM?

[11:30:30。487][http-nio-30666-exec-1][ERROR][。a。c。c。C。[。[。[/]。[dispatcherServlet]:175]- Servlet。service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java。lang。OutOfMemoryError: unable to create new native thread] with root causejava。lang。OutOfMemoryError: unable to create new native thread

可見OOM是因為無法建立執行緒,newCachedThreadPool這種執行緒池的最大執行緒數是Integer。MAX_VALUE,也可認為無上限,而其工作佇列SynchronousQueue是一個沒有儲存空間的阻塞佇列。

由於不知道Java執行緒池的bug,某程式設計師叕被祭天

所以只要有請求到來,就必須找到一條工作執行緒處理,若當前無空閒執行緒就再建立一個新的。由於我們的任務需很長時間才能執行完成,大量任務進來後會建立大量執行緒。而執行緒是需要分配一定記憶體空間作為執行緒棧的,比如1MB,因此無限建立執行緒必OOM

所以使用執行緒池,請不要抱任何僥倖,以為只是處理輕量任務,不會造成佇列積壓或建立大量執行緒!比如某業務一旦接受到請求,就會呼叫外部服務,該外部服務介面正常100ms內會響應,現在TPS過百,CachedThreadPool能穩定在佔用10個左右執行緒情況下滿足需求。可天有不測風雲,該外部服務不可用了!而程式碼裡呼叫該服務設定的超時又特別長,比如1min,1min可能已經進成千上萬請求,產生幾千個任務,需幾千個執行緒,沒多久就因為無法再建立新執行緒,OOM!

所以阿里才不建議使用Executors:

要結合實際併發情況,評估執行緒池核心引數,確保其工作行為符合預期,關鍵的也就是設定

有界工作佇列和數量可控的執行緒數

永遠要為自定義的執行緒池設定有意義名稱,以便排查問題因為當出現執行緒數量暴增、死鎖、CPU負載高、執行緒執行異常等事故時,往往都需抓取執行緒棧。有意義的執行緒名稱,就很重要。示例如下:

注意異常處理

透過ThreadPoolExecutor#execute()提交任務時,若任務在執行的過程中出現執行時異常,會導致

執行任務的執行緒

終止。但要命的是,有時任務雖然異常了,但你卻收不到任何通知,你還在開心摸魚,以為任務都執行很正常。雖然執行緒池提供了很多用於異常處理的方法,但最穩妥和簡單的方案還是捕獲所有異常並具體處理:

由於不知道Java執行緒池的bug,某程式設計師叕被祭天

執行緒池的執行緒管理

還好有谷歌,一般我們直接利用guava的ThreadFactoryBuilder實現執行緒池執行緒的自定義命名即可。

執行緒池的拒絕策略

執行緒池預設的拒絕策略會拋RejectedExecutionException,這是個執行時異常,IDEA不會強制捕獲,所以我們也很容易忽略它。對於採用何種策略,具體要看任務的重要性:

若是一些不重要任務,可選擇直接丟棄

重要任務,可採用降級,比如將任務資訊插入DB或MQ,啟用一個專門用作補償的執行緒池去補償處理。所謂降級,也就是在服務無法正常提供功能的情況下,採取的補救措施。具體處理方式也看具體場景而不同。

當執行緒數大於核心執行緒數時,執行緒等待keepAliveTime後還是無任務需要處理,收縮執行緒到核心執行緒數了解這個策略,有助於我們根據實際的容量規劃需求,為執行緒池設定合適的初始化引數。也可透過一些手段來改變這些預設工作行為,比如:

宣告執行緒池後立即呼叫prestartAllCoreThreads,啟動所有核心執行緒

傳true給allowCoreThreadTimeOut,讓執行緒池在空閒時同樣回收核心執行緒

彈性伸縮的實現

執行緒池是先用Q存放來不及處理的任務,滿後再擴容執行緒池。當Q設定很大時(那個工具類),最大執行緒數這個引數就沒啥意義了,因為佇列很難滿或到滿時可能已OOM,更沒機會去擴容執行緒池了。是否能讓執行緒池優先開啟更多執行緒,而把Q當成後續方案?比如我們的任務執行很慢,需要10s,若執行緒池可優先擴容到5個最大執行緒,那麼這些任務最終都可以完成,而不會因為執行緒池擴容過晚導致慢任務來不及處理。

難題在於:

執行緒池在工作佇列滿時,會擴容執行緒池重寫佇列的offer,人為製造該佇列滿的條件

改變了佇列機制,達到最大執行緒後勢必要觸發拒絕策略實現一個自定義拒絕策略,這時再把任務真正插入佇列

Tomcat就實現了類似的“彈性”執行緒池。

務必確認清楚執行緒池本身是不是複用的。

某服務偶爾報警執行緒數過多,但過一會兒又會降下來,但應用的請求量卻變化不大。

可以線上程數較高時抓取執行緒棧,發現記憶體中有上千個執行緒池,這肯定不正常!

但程式碼裡也沒看到聲明瞭執行緒池,最後發現原來是業務程式碼呼叫了一個類庫:

由於不知道Java執行緒池的bug,某程式設計師叕被祭天

該類庫竟然每次都建立一個新的執行緒池!

由於不知道Java執行緒池的bug,某程式設計師叕被祭天

newCachedThreadPool會在需要時建立必要數量的執行緒,業務程式碼的一次業務操作會向執行緒池提交多個慢任務,這樣執行一次業務操作就會開啟多個執行緒。如果業務操作併發量較大的話,的確有可能一下子開啟幾千個執行緒。

那為何監控中看到執行緒數量會下降,而不OOM?

newCachedThreadPool的核心執行緒數是0,而keepAliveTime是60s,所以60s後所有執行緒都可回收。

那這如何修復呢?

使用static欄位存放執行緒池引用即可

由於不知道Java執行緒池的bug,某程式設計師叕被祭天

執行緒池的意義在於複用,就意味著程式應該始終使用一個執行緒池嗎?

不,具體場景具體分析。

比如一個 I/O 型任務,不斷向執行緒池提交任務:向一個檔案寫入大量資料。執行緒池的執行緒基本一直處於忙碌狀態,佇列也基本滿。而且由於是

CallerRunsPolicy

策略,所以當執行緒滿佇列滿,任務會在提交任務的執行緒或呼叫execute方法的執行緒執行,所以不要認為提交到執行緒池的任務就一定會被

非同步處理

畢竟,若使用CallerRunsPolicy,就有可能

非同步任務變同步

執行。使用CallerRunsPolicy,當執行緒池飽和時,計算任務會在執行Web請求的Tomcat執行緒執行,這時就會進一步影響到其他同步處理的執行緒,甚至造成整個應用程式崩潰。

如何修正?

使用單獨的執行緒池處理這種“I/O型任務”,將執行緒數設定多一些!

所以千萬不要盲目複用別人寫的執行緒池!因為它不一定適合你的任務!

Java 8的parallel stream

可方便並行處理集合中的元素,共享同一ForkJoinPool,預設並行度是

CPU核數-1

。對於CPU繫結的任務,使用這樣的配置較合適,但若集合操作涉及同步I/O操作(比如資料庫操作、外部服務呼叫),建議自定義一個ForkJoinPool(或普通執行緒池)。

最後宣告一點:提交到相同執行緒池中的任務,一定要是相互獨立的,最好不要有依賴關係!

參考《阿里巴巴Java開發手冊》