Java 对多线程有良好的支持, 并且提供了方便使用的线程池框架 (Executor). 但如果使用不当, 可能会带来一些不安全的隐患. 本文将分享一次由于随意创建线程池造成线程数持续增加的问题.
一, 背景
首先看一个图, 下图是线上服务器 Java 线程数的监控图.
图中每个下降的点都是在该时间点有上线操作, Tomcat 重启的原因. 其他时间, 线程数呈线性增长趋势, 最高点已经快到 3 千了. 非常恐怖! 如果不是因为有频繁的上线操作, 线上服务很快就会出问题.
二, 问题调查分析
将监控图时间点往回拉, 定位到线程数异常开始的时间点. 查看当天提交记录, 发现一处与线程有关的修改. 代码如下:
- /**
- * 异步执行操作
- */
- private void asyncDoSomething() {
- ExecutorService executorService = Executors.newSingleThreadExecutor();
- ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1));
- executorService.submit(new Runnable() {
- @Override
- public void run() {
- // 此处仅使用示例代码
- System.out.println("do something async...");
- }
- });
- }
我们先不讨论此处线程池使用是否正确, 仅就此处修改而言, 将原有 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, 如所示:
- DefaultThreadFactory() {
- SecurityManager s = System.getSecurityManager();
- group = (s != null) ? s.getThreadGroup() :
- Thread.currentThread().getThreadGroup();
- namePrefix = "pool-" +
- poolNumber.getAndIncrement() +
- "-thread-";
- }
目前基本确定问题该问题是此处使用 ThreadPoolExecutor 引起的. 其实原因不复杂: 程序每次调用 asyncDoSomething 方法时, 均会创建一个新的线程池来执行任务. 但在执行任务后并未关闭该线程池, 造成线程无法被回收, 线程一直处于等待状态. 因而线程数会随时间线性上升.
2. 分析 Executors 创建线程池方式
为什么原来使用 Executors.newSingleThreadExecutor() 时未出现这个问题呢? 仍然是查看源码:
- public static ExecutorService newSingleThreadExecutor() {
- return new FinalizableDelegatedExecutorService
- (new ThreadPoolExecutor(1, 1,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>()));
- }
原来该方法并不是直接 new 一个 ThreadPoolExecutor 对象返回, 而是使用了一个代理类进行代理. 进一步查看 FinalizableDelegatedExecutorService 源码:
- static class FinalizableDelegatedExecutorService
- extends DelegatedExecutorService {
- FinalizableDelegatedExecutorService(ExecutorService executor) {
- super(executor);
- }
- protected void finalize() {
- super.shutdown();
- }
- }
在这个代理类中, 实现了 finalize 方法, 并在 finalize 方法中关闭线程池. 根据 finalize 的特性, 在 GC 时会调用 finalize 方法. 因此 Executors.newSingleThreadExecutor() 在每次垃圾回收时触发未被使用的线程池关闭, 所以没有出现线程数持续上升的问题.
三, 总结
这个问题是由于线程池使用不当造成的. 使用线程池是为了避免重复, 频繁地创建, 销毁线程, 进而对多个线程进行复用. 以上线程池的使用明显未达到该目的, 并因为线程池未关闭而造成线程无法被回收, 线程数持续增加.
对以上代码进行修改后如下:
- /** 固定大小线程池: 核心线程数 10, 最大线程数 10, 空闲线程存活时长 120 秒, 等待队列无界 */
- private static final ExecutorService EXECUTOR_SERVICE = new ThreadPoolExecutor(10,
- 10,
- 120L,
- TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>(),
- new ThreadFactoryBuilder().setNameFormat("do-something-thread-pool-%d").build(),
- new ThreadPoolExecutor.AbortPolicy());
- /**
- * 异步执行操作
- */
- private void asyncDoSomething() {
- EXECUTOR_SERVICE.submit(new Runnable() {
- @Override
- public void run() {
- // 此处仅使用示例代码
- System.out.println("do something async...");
- }
- });
- }
定义一个统一的线程池, 在每次调用 asyncDoSomething 方法时, 都向该线程池提交一个任务.
修改后, 线程数维持在一个比较稳定的量.
来源: https://www.cnblogs.com/ethanzhong/p/10339366.html