Java對多線程有良好的支持,並且提供了方便使用的線程池框架(Executor)。但如果使用不當,可能會帶來一些不安全的隱患。本文將分享一次由於隨意創建線程池造成線程數持續增加的問題。
一、背景
首先看一個圖,下圖是線上服務器Java線程數的監控圖。
圖中每個下降的點都是在該時間點有上線操作,Tomcat重啟的原因。其他時間,線程數呈線性增長趨勢,最高點已經快到3千了。非常恐怖!如果不是因為有頻繁的上線操作,線上服務很快就會出問題。
二、問題調查分析
將監控圖時間點往回拉,定位到線程數異常開始的時間點。查看當天提交記錄,發現一處與線程有關的修改。代碼如下:
1 /** 2 * 異步執行操作 3 */ 4 private void asyncDoSomething() { 5 ExecutorService executorService = Executors.newSingleThreadExecutor(); 6 ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1)); 7 executorService.submit(new Runnable() { 8 @Override 9 public void run() { 10 // 此處僅使用示例代碼 11 System.out.println("do something async..."); 12 } 13 }); 14 }
我們先不討論此處線程池使用是否正確,僅就此處修改而言,將原有 Executors.newSingleThreadExecutor() 替換為 new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1)) ,似乎並無不妥(這么修改,是為了遵循阿里規約)。實現的功能都是創建一個單線程池。
1.dump線程棧分析
既然代碼上未發現明顯問題,那就轉而直接查看線上問題。執行 $jps -v 查找到Java程序對應的進程號,然后執行 $jstack ${pid_num} > thread_dump.log ,將對應Java程序的線程棧信息轉儲到thread_dump.log文件中。(注意,如果當前操作用戶不是啟動Java程序的用戶,需要執行 $sudo -u user_name jstack ${pid_num} > thread_dump.log )。
截取部分線程棧信息如下:
"pool-165671-thread-1" #188938 prio=5 os_prio=0 tid=0x00007f1a38040000 nid=0x7f19 waiting on condition [0x00007f19065b9000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000dbb0a178> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Locked ownable synchronizers: - None "pool-164990-thread-1" #188175 prio=5 os_prio=0 tid=0x00007f1a5402c800 nid=0x7a61 waiting on condition [0x00007f18d0d5e000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000d8c1ef78> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Locked ownable synchronizers: - None
線程棧信息即是在java程序中,所有線程的調用棧信息。其中包含了線程名、線程當前狀態等內容。
經統計發現,當前Java程序一共有845個線程,其中803個線程處於線程阻塞等待狀態:WAITING (parking)。而所有該狀態的線程名字均為 pool-xxxxx-thread-1 ,即該線程屬於某單線程池。
進一步分析 ThreadPoolExecutor 源碼后發現,ThreadPoolExecutor 默認使用 DefaultThreadFactory 構造的線程池前綴即為pool-xxxxx-thread-1 ,如所示:
1 DefaultThreadFactory() { 2 SecurityManager s = System.getSecurityManager(); 3 group = (s != null) ? s.getThreadGroup() : 4 Thread.currentThread().getThreadGroup(); 5 namePrefix = "pool-" + 6 poolNumber.getAndIncrement() + 7 "-thread-"; 8 }
目前基本確定問題該問題是此處使用 ThreadPoolExecutor引起的。其實原因不復雜:程序每次調用asyncDoSomething方法時,均會創建一個新的線程池來執行任務。但在執行任務后並未關閉該線程池,造成線程無法被回收,線程一直處於等待狀態。因而線程數會隨時間線性上升。
2.分析Executors創建線程池方式
為什么原來使用 Executors.newSingleThreadExecutor() 時未出現這個問題呢?仍然是查看源碼:
1 public static ExecutorService newSingleThreadExecutor() { 2 return new FinalizableDelegatedExecutorService 3 (new ThreadPoolExecutor(1, 1, 4 0L, TimeUnit.MILLISECONDS, 5 new LinkedBlockingQueue<Runnable>())); 6}
原來該方法並不是直接new一個ThreadPoolExecutor對象返回,而是使用了一個代理類進行代理。進一步查看 FinalizableDelegatedExecutorService 源碼:
1 static class FinalizableDelegatedExecutorService 2 extends DelegatedExecutorService { 3 FinalizableDelegatedExecutorService(ExecutorService executor) { 4 super(executor); 5 } 6 protected void finalize() { 7 super.shutdown(); 8 } 9 }
在這個代理類中,實現了finalize方法,並在finalize方法中關閉線程池。根據finalize的特性,在GC時會調用finalize方法。因此 Executors.newSingleThreadExecutor()在每次垃圾回收時觸發未被使用的線程池關閉,所以沒有出現線程數持續上升的問題。
三、總結
這個問題是由於線程池使用不當造成的。使用線程池是為了避免重復、頻繁地創建、銷毀線程,進而對多個線程進行復用。以上線程池的使用明顯未達到該目的,並因為線程池未關閉而造成線程無法被回收,線程數持續增加。
對以上代碼進行修改后如下:
1 /** 固定大小線程池:核心線程數10,最大線程數10,空閑線程存活時長120秒,等待隊列無界 */ 2 private static final ExecutorService EXECUTOR_SERVICE = new ThreadPoolExecutor(10, 3 10, 4 120L, 5 TimeUnit.MILLISECONDS, 6 new LinkedBlockingQueue<Runnable>(), 7 new ThreadFactoryBuilder().setNameFormat("do-something-thread-pool-%d").build(), 8 new ThreadPoolExecutor.AbortPolicy());
1 /** 2 * 異步執行操作 3 */ 4 private void asyncDoSomething() { 5 EXECUTOR_SERVICE.submit(new Runnable() { 6 @Override 7 public void run() { 8 // 此處僅使用示例代碼 9 System.out.println("do something async..."); 10 } 11 }); 12 }
定義一個統一的線程池,在每次調用asyncDoSomething方法時,都向該線程池提交一個任務。
修改后,線程數維持在一個比較穩定的量。