爲什麼阿里不建議用excutors創建線程池

1 前言:


        大家都知道,阿里規範中有一條是不允許用excutors去創建線程池,而是採用ThreadPoolExecutor的原生方式去創建。很早就聽過所過這種說法,但是一直都沒去搞清楚是爲什麼,今天就查閱資料去了解了這個問題。

2 Excutors創建線程的方式


    通過Excutors來創建線程池,有4種創建線程的方法。

  • newCachedThreadPool 創建一個可緩存線程池,如果線程池的大小超過了處理任務所需的線程,那麼就會回收部份空閒線(60秒不處理任務)線程,當任務數增加時,此線程有可以智能的添加新線程來處理任務。此線程池不會對線程池的大小做限制,線程池的大小完全依賴於操作系統能夠創建的最大線程池大小。

  • newFixedThreadPool 創建固定大小線程池,提交一個任務就創建一個線程。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。

  • newSingleThreadExecutor 創建一個單線程化的線程池,這個線程池只有一個線程池在工作。如果這個唯一的線程因爲異常結束,那麼就會有一個新的線程來替代它,此線程池保證所有任務的執行順序按照任務的提交順序來執行。

  • newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。(自己可以點進源碼中看,本質也是調用了ThreadPoolExecutor()方法來實現的。

 大家看以上4個創建線程池的方式,可以發現其實最終都是調用了ThreadPoolExecutor()方法來實現的。(這裏不展開,之後會特意講到)

 一般不採用excutors直接創建線程池可以防止OOM,同時也可以更好的理解線程池的構建原理。

我們來用excutors直接創建線程池模仿一個產生OOM異常的場景。

代碼塊:我們用newFixedThreadPool創建一個固定大小的線程池。讓其一直去執行任務,並且爲了更好的模擬OOM,我們設置VM options

public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        while (true) {
            executorService.execute(new Task());
        }
    }
 
static class Task implements Runnable{
 
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

運行結果:

 我們發現GC了。

這是我們可以用jps找到運行類的pid並且執行jstack 13548>13548.log。

13548.log:

2021-11-08 11:01:48
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.112-b15 mixed mode):
 
"DestroyJavaVM" #19 prio=5 os_prio=0 tid=0x00000000034d4000 nid=0x2084 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"pool-1-thread-5" #18 prio=5 os_prio=0 tid=0x0000000016b5b000 nid=0x21e8 waiting on condition [0x00000000177af000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at com.liubujun.thread.TestExcutor$Task.run(TestExcutor.java:27)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
 
"pool-1-thread-4" #17 prio=5 os_prio=0 tid=0x0000000016b58800 nid=0x20b8 waiting on condition [0x00000000176ae000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at com.liubujun.thread.TestExcutor$Task.run(TestExcutor.java:27)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
 
"pool-1-thread-3" #16 prio=5 os_prio=0 tid=0x0000000016b55800 nid=0x2618 waiting on condition [0x00000000175ae000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at com.liubujun.thread.TestExcutor$Task.run(TestExcutor.java:27)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
 
"pool-1-thread-2" #15 prio=5 os_prio=0 tid=0x0000000015e47000 nid=0x4bd0 waiting on condition [0x00000000174ae000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at com.liubujun.thread.TestExcutor$Task.run(TestExcutor.java:27)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
 
"pool-1-thread-1" #14 prio=5 os_prio=0 tid=0x0000000015e46800 nid=0xccc waiting on condition [0x00000000173ae000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at com.liubujun.thread.TestExcutor$Task.run(TestExcutor.java:27)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
 
"Service Thread" #13 daemon prio=9 os_prio=0 tid=0x0000000015dd0000 nid=0x3b04 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"C1 CompilerThread3" #12 daemon prio=9 os_prio=2 tid=0x0000000015d17000 nid=0x854 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"C2 CompilerThread2" #11 daemon prio=9 os_prio=2 tid=0x0000000015d0f800 nid=0x48b4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"C2 CompilerThread1" #10 daemon prio=9 os_prio=2 tid=0x0000000015d0c800 nid=0x5504 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"C2 CompilerThread0" #9 daemon prio=9 os_prio=2 tid=0x0000000015d0c000 nid=0x41c8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"JDWP Command Reader" #8 daemon prio=10 os_prio=0 tid=0x0000000015af8800 nid=0x5090 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"JDWP Event Helper Thread" #7 daemon prio=10 os_prio=0 tid=0x0000000015af5800 nid=0x5294 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"JDWP Transport Listener: dt_socket" #6 daemon prio=10 os_prio=0 tid=0x0000000015ae9000 nid=0x610c runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000000015ae1000 nid=0x4df0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000000015a8a000 nid=0xc9c runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000000015a71000 nid=0x6edc in Object.wait() [0x0000000015f4e000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000000ffcc8e60> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
    - locked <0x00000000ffcc8e60> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
    at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
 
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000013b7d000 nid=0x10f0 in Object.wait() [0x0000000015a4e000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000000ffca5840> (a java.lang.ref.Reference$Lock)
    at java.lang.Object.wait(Object.java:502)
    at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
    - locked <0x00000000ffca5840> (a java.lang.ref.Reference$Lock)
    at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
 
"VM Thread" os_prio=2 tid=0x0000000013b79000 nid=0x5724 runnable 
 
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000034ea000 nid=0x91c runnable 
 
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000034eb800 nid=0x6130 runnable 
 
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000034ed000 nid=0x3bb0 runnable 
 
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000034ee800 nid=0x4dd0 runnable 
 
"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x00000000034f0800 nid=0x51f4 runnable 
 
"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x00000000034f2000 nid=0x10e4 runnable 
 
"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x00000000034f6000 nid=0x540 runnable 
 
"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x00000000034f7000 nid=0x20b4 runnable 
 
"VM Periodic Task Thread" os_prio=2 tid=0x0000000015e17000 nid=0x621c waiting on condition 
 
JNI global references: 1795


 但我們明明已經設置了固定線程數量爲5。已經有線程去執行任務,爲什麼還會發生OOM呢。我們根據報錯信息最後發現是ThreadPoolExecutor.execute的1361行出錯,我們追蹤來看

 發現報錯這一行是一個if的判斷,前面是一個線程池的狀態判斷,不會出現OOM,原因就出在workQueue.offer(command)),我們可以追蹤一下這個方法發現其有多個實現,而我們用newFixedThreadPool的阻塞隊列用的是LinkedBlockingQueue<Runnable>()

我們追蹤到最後發現是416行,new了一個節點,然後將我們的任務放了進去。因爲我的代碼了任務是sleep了10秒鐘,所以線程任務的執行速度遠遠小於線程隊列的創建速度,我一直都在向任務對列中放任務,它就會一直new Node知道空間內存不夠發生OOM。

所以當我們的代碼耗時較長時並且又採用了excutors去創建線程池,就有可能發生OOM風險。因此一般採用ThreadPoolExecutor()來創建線程池,設置合理的參數和拒絕策略。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章