被開發者拋棄的 Executors,錯在哪兒?

被開發者拋棄的 Executors,錯在哪兒?

一. 序

在 Java 領域內,我們使用多執行緒的方式來實現併發程式設計。而執行緒本身是作業系統的一個概念,雖然不同的語言對執行緒都進行了一些封裝,但是最終都是呼叫到作業系統中去建立和排程執行緒。

既然執行緒是一項重要的系統資源,為了更合理的利用此資源,我們會使用池化技術來最佳化執行緒的建立和銷燬,這就是

執行緒池

在我們學習併發程式設計的時候,執行緒可以利用 Thread 來建立並透過 start() 來啟動一個線,但在成熟的專案中,基本上是不允許這樣操作執行緒的,都需要透過執行緒池去收斂執行緒的使用,所以執行緒池是必須的。

Java 的執行緒池可以透過 ThreadPoolExecutor 來構造,在其中提供非常完備的構造方法,可以根據我們的業務需求靈活的構造執行緒池。同時 Java 還提供了一個 Executors,它內部提供了很多包裝的方法,利用它可以幫我們快速的構建執行緒池。

原本 Executors 的目的就是為了讓我們更方便的使用執行緒池,但是《阿里巴巴Java開發手冊》也明確指出,直接使用 Executors 的缺陷。

手冊中提到強制不允許使用 Executors 去建立執行緒池,而是應該使用退化到最原始的 ThreadPoolExecutor 的方式。

被開發者拋棄的 Executors,錯在哪兒?

日常開發中,應該收緊對執行緒池的建立,由開發人員明確執行緒池的執行規則,以此來儘量規避其資源耗盡的風險。

執行緒池是個好東西,但是怎麼建立是一個問題。

二. Executors 怎麼了?

2。1 不被允許的 Executors

不應該使用 Executors 的原因,其實《阿里巴巴Java開發手冊》裡已經寫明瞭,當需要處理大量任務的時候,可能會出現 OOM 異常,但它們出現 OOM 的原因並不一樣。

ThreadPoolExecutor 的構造方法中,提供了很多引數的配置,其中與 Executors 出現 OOM 相關的就有 2 個:

核心執行緒數

等待佇列

先來看看 FixedThreadPool 和 SingleThreadPool 出現 OOM 的原因。

它們的問題在於等待佇列使用了 LinkedBlockingQueue 這個以連結串列實現的無界佇列(最大長度是 Integer。MAX_VALUE),最終導致堆積了大量等待處理的任務,從而導致頻繁的 GC,最終觸發 OOM。

java。lang。OutOfMemoryError: GC overhead limit exceeded

再來看看 CachedThreadPool 出現 OOM 的原因。

它的問題在於核心執行緒數設定為了 Integer。MAX_VALUE,並且等待佇列是一個 SynchronousQueue。

SynchronousQueue 是一個沒有資料緩衝的阻塞佇列,它極易被阻塞。在等待佇列被阻塞的時候,如果執行緒數量還沒有達到核心執行緒數限制的數量時,執行緒池的策略是建立新的執行緒來處理新的任務。

也就是說,是核心執行緒數和等待佇列 SynchronousQueue 合力造成了執行緒會跟隨任務不斷的被建立,直到觸發 OOM。

java。lang。OutOfMemoryError: pthread_creat (1040KB stack) failed: Try again

ScheduledThreadPool 的等待佇列使用的是 DelayedWorkQueue,原理也是類似的,最終會導致建立大量的執行緒而丟擲 OOM。

執行緒是一種系統資源,本身建立就會帶來記憶體開銷,同時作業系統對單程序可建立的執行緒數也是有限制的。

在 Android 中,每個執行緒初始化都需要 mmap 一定的堆記憶體,在預設的情況下,初始化一個執行緒大約需要 mmap 1MB 左右的記憶體空間。同時系統本身也會對每個程序可建立的執行緒數,做一定的限制,這個限制在 /proc/pid/limits 中,不同的廠商對這個限制也有所不同,當超出限制時,哪怕堆上還有可用記憶體,依然會丟擲 OOM。

2。2 Executors 錯在哪兒了?

Executors 會在任務過多的時候,導致資源耗盡而觸發 OOM,這是它帶來的危害。

Executors 最大的問題,在於沒有邊界。

在系統環境良好,任務不多的時候 Executors 建立的執行緒池,都是可以正常工作的。

但是一旦有重壓,我們就無法預知什麼時候會出現問題,這就是沒有邊界,沒有邊界就意味著不可控。

我們很難去信任一段不可控的程式碼,它什麼時候出現問題,完全是不可預知的,這才是 Executors 最大的問題。

除此之外,Executors 封裝了太多執行緒池的細節,本身也不建議使用。例如通常我們需要給執行緒池建立的執行緒,起一個有意義的名稱,方便在出現異常的時候排查問題;再例如對與執行緒池的拒絕策略,我們需要深思熟慮的定義,是直接拋棄還是持久化下來延遲處理。

去思考一個執行緒池的不同引數帶來的策略細節,才是使用執行緒池的一個良好的開發習慣。

三. 小結時刻

本文我們聊了關於建立執行緒池,使用 Executors 建立的執行緒池會有 OOM 的風險,應該使用 ThreadPoolExecutor 去建立執行緒池。透過思考業務來明確配置執行緒池不同的引數,例如執行緒池、等待佇列、拒絕策略等等。

今天就到這裡,有任何問題,歡迎留言討論。

本文對你有幫助嗎?

留言、轉發、點贊

是最大的支援,謝謝!

在頭條號私信我。我會送你一些我整理的學習資料,包含:Android反編譯、演算法、設計模式、虛擬機器、Linux、Kotlin、Python、爬蟲、Web專案原始碼。