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()來創建線程池,設置合理的參數和拒絕策略。