在配置线程池核心线程数大小和最大线程数大小后,如果调用线程池setCorePoolSize方法来调整线程池中核心线程的大小,需要特别注意,可能踩坑,说不定增加了线程让你的程序性能更差。
ThreadPoolExecutor有提供一个动态变更线程池核心线程数大小的方法setCorePoolSize,如果我们使用次方法来改变线程池的线程数确实可以实现想要的效果,但是会有一些意想不到的结果。比如,出现下图的情况。
先简单介绍一下背景,我先创建了一个核心线程数和最大线程数都为1的线程池,然后通过ThreadPoolExecutor.setCorePoolSize()方法设置核心线程数为5,接着继续往线程池中丢任务,我看通过截图可以看到,这些线程在执行完一次任务后就直接销毁了,并不是想像中的执行完一个任务后持续的执行队列中的任务或者是阻塞等待新任务的到来。如果一直持续这样,系统的性能其实是会更差的,因为一直在创建线程销毁线程。
但是,如果我是创建一个核心线程数和最大线程数都为5的线程池,然后通过ThreadPoolExecutor.setCorePoolSize()方法设置核心线程数为1或者2。如果我持续往里面丢任务,核心线程数也是保持在我设定的值。
小结
如果此时是将线程数调小没有任何问题,线程在处理完任务以及队列中的任务后会阻塞在队首等待任务到来被唤醒。
如果此时是将线程数调大就需要注意,当有任务到来时,如果当前核心线程数小于配置的,则会创建新的线程,但是它执行完一次任务后就会被销毁,即使队列中还有任务等待执行,他也会立马被销毁。
问题排查
出现问题第一反应就是线程在执行完当前任务后,获取任务的时候出了问题,于是在java.util.concurrent.ThreadPoolExecutor#getTask方法中获取队列任务的地方打断点排查是否是获取任务出现问题。初步怀疑是这里timed判断有问题导致的,结果断点都没有到这里。
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;
}
以为是别的地方有问题,转了一圈回来还是怀疑这个方法,最后仔细看了一遍,发现了问题所在。
出现以上问题的根本原因就是我们只动态修改了核心线程数大小,未同步修改最大线程数大小。每一次任务执行完成后需要调用java.util.concurrent.ThreadPoolExecutor#getTask方法,其中有一段下面的逻辑。由于我们只调整了核心线程数,未同步修改最大线程数,这里就会走到returen null的逻辑,就会退出当前线程。
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
总结
在动态修改线程的核心线程数后也需要同步修改最大线程数,否则会导致线程创建后执行一个任务后就被销毁。