记录一次由于线程池引发的生产事故
问题描述:
服务器在线上环境正常运行了半个多月,每天的跑批都会发送一条开始跑批的短信记录,突然某一天没有发送短信。排查该问题,发现发送短信是通过线程池异步发送的,查找短信的发送日志记录,发现没有任何的日志,看日志也没有任何的报错信息。按道理来说如果说线程池的任务满了应该丢弃任务跑出异常的,因为线程池采取的策略就是AbortPolicy策略。
原因分析:
现在系统还可以正常运行,不可能是因为内存溢出的问题,那么很可能就是线程池的问题,于是查看代码发现
public static ExecutorService newFixedThreadPool(int nThreads) { String poolName = "default"; //队列是无限队列 //JDK的newFixedThreadPool 默认的拒绝策略为:AbortPolicy return new ThreadPoolMonitor(nThreads, nThreads, 0L,TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new GcollThreadFactory(poolName), new ThreadPoolExecutor.AbortPolicy(),poolName); }这里是通过工具类生成的一个线程池,核心线程数与最大线程数设置的都是32,线程存活的时间是0秒,采取的是Linked阻塞队列,封装了一个线程池工厂用来给生成的线程起别名,采取的拒绝策略是AbortPolicy策略,丢弃任务并抛出异常。
我们看一下ThreaPoolExecutor的源码解释:
* <li> If corePoolSize or more threads are running, the Executor * always prefers queuing a request rather than adding a new * thread.</li>翻译如下:如果超过线程池的核心线程池的数量在运行,再来的请求会进入到队列而不是添加一个新的请求。
new LinkedBlockingQueue<Runnable>(),这个方法的默认容量是Integer的最大值。
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); } /** * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. * * @param capacity the capacity of this queue * @throws IllegalArgumentException if {@code capacity} is not greater * than zero */ public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); }所以只要前面32个核心线程都被一直占用住了,那么再来的请求会一直加入到阻塞队列中,所以没有报错,也没有采取拒绝策略。
通过jstack PID命令查看运行中的线程:
发现这些线程都在同一处进行睡眠,我们排查代码发现:
发现是一个同事写的循环睡眠,这是一个定时任务,每次的定时任务执行到这里因为条件不满足都会卡在这里,一直睡眠,导致线程池的线程耗尽,最终来的新请求都处理不了也不报错。
解决方案:
这里在循环等待某个条件的时候加一个超时时间就可以了,切记条件不满足的死循环。

