Error message here!

Hide Error message here!

忘记密码?

Error message here!

请输入正确邮箱

Hide Error message here!

密码丢失?请输入您的电子邮件地址。您将收到一个重设密码链接。

Error message here!

返回登录

Close

一次排查Java线程数异常--线程池使用不当造成线程数升高

EthanZhong 2019-01-30 18:31:00 阅读数:210 评论数:0 点赞数:0 收藏数:0

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方法时,都向该线程池提交一个任务。

修改后,线程数维持在一个比较稳定的量。

 

版权声明
本文为[EthanZhong]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/ethanzhong/p/10339366.html

编程之旅,人生之路,不止于编程,还有诗和远方。
阅代码原理,看框架知识,学企业实践;
赏诗词,读日记,踏人生之路,观世界之行;

支付宝红包,每日可领