被開發者拋棄的 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。

<code>java.lang.OutOfMemoryError: GC overhead limit exceeded/<code> 

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

它的問題在於核心線程數設置為了 Integer.MAX_VALUE,並且等待隊列是一個 SynchronousQueue。

SynchronousQueue 是一個沒有數據緩衝的阻塞隊列,它極易被阻塞。在等待隊列被阻塞的時候,如果線程數量還沒有達到核心線程數限制的數量時,線程池的策略是創建新的線程來處理新的任務。

也就是說,是核心線程數和等待隊列 SynchronousQueue 合力造成了線程會跟隨任務不斷的被創建,直到觸發 OOM。

<code>java.lang.OutOfMemoryError: pthread_creat (1040KB stack) failed: Try again/<code>

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項目源碼。


分享到:


相關文章: