写在前面
这个话题来源于线上环境的一次真实问题定位,现象是分析dump文件发现线程池大多数线程都处于TIMED_WAITING或者是WAITING状态,其实这也不是什么大问题,线程数也不算太多,任务队列也没有堆积,本着对技术的学习和优化态度开始了研究之路
什么是TIMED_WAITING和WAITING状态
先列一下线程的几种状态
- 初始(NEW):新创建线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java 虚拟机中线程的可运行状态包括操作系统中线程的就绪(ready)和运行中(running)两种状态。即处于该状态时,线程可能正在等待来自操作系统的其他资源(如CPU)。
- 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
下面看下线程的状态变化图
很明显现在状态转换为TIMED_WAITING有5中方法,但是常见的还有Object.wait(long)和LockSupport.parkNaons(),WAITING状态也类似。
问题分析
分析1
经过堆栈分析,发现本次线上问题主要是因为线程池任务队列为空,各个线程一直处于等待任务的过程。但是通过监控发现活跃线程数还不及核心线程数的一半,但是存活线程数已经达到了最大线程数。举个例子,现在核心是32, 最大是200,但是现在活跃的,也就是正在跑任务的线程个数也就是20的样子,但是活跃的+等待的线程数却是打满到了200。
分析2
这个时候我就产生了疑问,活跃的也就20,还没到核心线程数,任务也没堆积,为什么空闲线程没有回收呢。带着这个疑问我进行了如下操作:
- 本地复现
首先确认是不是代码设置问题,排除了这一层之后,我在本地起了服务,然后起了1000个线程压测,用visualvm监测,发现线程数先是瞬间打满到200,然后慢慢的减少,最后稳定在32,也就是说空闲线程被回收了。
- 分析怀疑的点
确定代码层面没有问题之后,开始第二层分析。因为线上环境和本地环境最大的问题就是线上环境流量较大而且不间断,我就怀疑是线上环境下,线程池空闲线程没有等到超时就被新的任务唤醒去执行任务了,然后任务执行完之后发现队列没有任务了(被其他线程申请走了),就重新开始等待,所以线程几乎一直处于限时等待状态。
- 资料查询
在google上重点查询了,线程池线程回收机制,最后发现了这篇
https://blog.csdn.net/zhujiangtaotaise/article/details/122358731
这里面主要讲的是线程池分配任务给线程是按轮询机制的,底层是基于AQS中的等待队列
- 源码分析
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
重点看workQueue.poll()这个方法,当设置allowCoreThreadTimeOut为true或者工作线程数大于核心线程数就会进入到这里。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
if (nanos <= 0L)
return null;
// 发现队列为空,通过 ReentrantLock 的 Condition 来实现阻塞等待线程存活时间
nanos = notEmpty.awaitNanos(nanos);
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
这里队列使用的是LinkedBlockingQueue,实际上到这也差不多明晰了,后面都是 AQS队列的内容了,具体的可以查阅AQS相关,总之就是线程成功申请到任务执行之后就将线程放到队尾,相当于重新开始计时,所以一直处于等待状态
其他
-
线程池也有很多其他需要注意的点,不同的场景有不同的用法,比如对性能要求很高,需要快速响应,就需要将队列设置为0,或者修改策略,先增加线程数到最大线程数,再放入队列,保证任务快速执行。
-
线程池也提供了预热接口 prestartAllCoreThreads,预热单一或者所有核心线程,可以减少一开始流量过来线程创建的损耗。
小结
其实线程池最复杂的还是参数的设置,包括核心,最大,队列大小,拒绝策略,空闲时间,甚至采用何种队列。也有很多看似精细的算法来计算线程数,但是实际上很依靠开发者的工程经验。我们项目中大多还是采用线程池监控+动态线程池来达到最佳使用的目的!