2011-05-16 64 views
8

保羅季馬presentation有這樣一行:爲什麼通過newCachedThreadPool創建的ExecutorService是邪惡的?

Executors.newCacheThreadPool邪惡,死死死

爲什麼是邪惡?

我會冒險猜測:這是因爲線程的數量會以無限制的方式增長。因此,如果達到JVM的最大線程數,那麼已經被slashdotted的服務器可能會死亡?

+1

我已經使用了一個緩存池,這個緩存池非常危及我的Windows Vista系統,我不得不重啓它。任務管理器不會迴應。 – 2011-05-16 13:15:47

+0

因爲相同的原因來到這裏:) – Eugen 2013-05-01 15:43:51

回答

8

Executors.newCacheThreadPool()的問題是,執行程序將創建並啓動儘可能多的線程以執行提交給它的任務。儘管完成的線程被釋放(閾值是可配置的)的事實可以緩解這種情況,但確實會導致嚴重的資源匱乏,甚至導致JVM(或某些設計不合理的操作系統)崩潰。

4

有幾個問題。線程方面的無限增長是一個顯而易見的問題 - 如果你有cpu綁定的任務,那麼允許比可用的CPU運行更多的任務就是創建調度程序開銷,而你的線程上下文切換到所有地方,而實際上沒有任何進展。如果你的任務是IO界限,但事情變得更加微妙。瞭解如何調整正在等待網絡或文件IO的線程池的大小要困難得多,並且很大程度上取決於這些IO事件的延遲。更高的延遲意味着您需要(並且可以支持)更多的線程。

由於任務生產速度超過執行速度,緩存的線程池將繼續添加新線程。對此有幾個小障礙(比如鎖定串行化新的線程ID創建),但這可以解除綁定增長可能導致內存不足錯誤。

緩存線程池的另一個大問題是它對於任務生產者線程來說可能很慢。該池配置有SynchronousQueue用於提供任務。此隊列實現基本上具有零大小,並且只有在生產者存在匹配的使用者時纔有效(當另一個提供者時有線程輪詢)。 Java6中的實際實現得到了顯着改進,但對於生產者來說,實際實現仍然相對較慢,特別是在生產者失敗時(因爲生產者然後負責創建新線程以添加到池中)。對於生產者線程而言,通常將任務簡單地放在實際隊列上並繼續運行通常更爲理想。

問題是,沒有人擁有一個擁有一小組核心線程的池,當它們都忙時,創建新線程達到某個最大值,然後排入後續任務。固定線程池似乎承諾這一點,但它們只在底層隊列拒絕更多任務(已滿)時纔開始添加更多線程。一個LinkedBlockingQueue永遠不會滿,所以這些池永遠不會超出核心大小。一個ArrayBlockingQueue有一個容量,但因爲它只在容量達到時才增長,所以這不會降低生產率,直到它已經是一個大問題。目前該解決方案需要使用諸如呼叫者運行的良好rejected execution policy,但需要小心。

開發人員看到緩存的線程池並盲目使用它,卻沒有真正考慮後果。

16

(這是保羅)

幻燈片的意圖是(除了有開玩笑的措辭),作爲你提到,該線程池的增長,而不綁定創建新線程。

線程池固有地表示系統中的隊列和傳輸點。也就是說,有些東西正在爲它的工作提供幫助(並且它也可能在其他地方進行工作)。如果線程池開始增長,因爲它跟不上需求。

一般來說,這很好,因爲計算機資源是有限的,而且隊列的構建是爲了處理突發性工作。但是,該線程池並不能控制能夠推動瓶頸。

例如,在服務器場景中,有幾個線程可能正在接受套接字並將線程池移交給客戶端進行處理。如果該線程池開始失控 - 系統應停止接受新客戶端(實際上,「acceptor」線程會經常跳入線程池以幫助處理客戶端)。

如果使用帶有無界輸入隊列的固定線程池,效果類似。每當你考慮到隊列失控的場景 - 你就會意識到這個問題。

IIRC,馬特威爾士的開創性SEDA服務器(它們是異步的)創建了根據服務器特性修改其大小的線程池。

停止接受新客戶端的想法聽起來很糟糕,直到你意識到替代方案是一個沒有處理客戶端的癱瘓系統。 (同樣,理解計算機是有限的 - 即使是最優調整的系統也有限制)

順便提一下,JVM根據JVM將線程限制爲16k(通常)或32k線程。但是如果你受到CPU限制,那麼這個限制就不是很相關了 - 在CPU綁定系統上啓動另一個線程會適得其反。

我很高興地運行4或5千線程的系統。但是,接近16k的限制,即使沒有CPU綁定,事情也往往會停滯下來(這個限制JVM強制執行 - 我們在linux C++中有更多的線程)。

+0

鑑於這是原始報價的作者,他提供了最全面的回覆,我覺得這應該是公認的答案。 – KomodoDave 2013-09-11 10:24:23