一次排查Java線程數異常--線程池使用不當造成線程數升高


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方法時,都向該線程池提交一個任務。

修改后,線程數維持在一個比較穩定的量。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM