总结:
BlockingQueue 在通知空闲线程获取任务时,是通过信号量的方式,每次唤醒一个线程去取任务,所以,当向线程池提交任务速率 > BlockingQueue 通知线程取任务速率时(即每次唤醒一个线程去执行任务处理不过来),新提交的任务便会执行对应的拒绝策略。
业务场景:
对账进程需要下载 某一时间段 的 n个账号 的第三方账单,第三方账单中数据量大,需要控速入库,入库完一组数据,再去下载第二组账单。
线程池配置:
@Bean
public ThreadPoolTaskExecutor cpuThreadPool() {
ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
poolTaskExecutor.setCorePoolSize(4);
poolTaskExecutor.setMaxPoolSize(5);
poolTaskExecutor.setQueueCapacity(2);
poolTaskExecutor.setKeepAliveSeconds(60);
poolTaskExecutor.setAllowCoreThreadTimeOut(false);
poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
poolTaskExecutor.setAwaitTerminationSeconds(20);
return poolTaskExecutor;
}
问题描述:
Demo:
@RunWith(SpringRunner.class)
@SpringBootTest
public class ThreadPoolInSpring {
@Resource
private ThreadPoolTaskExecutor cpuThreadPool;
@Test
public void contextLoads() throws Exception {
// 下载十天的账单
for (int i = 0; i < 10; i++) {
// 每次执行一批任务
doOnceTasks(i);
System.out.println("第" + i + "天的下载");
}
}
/**
* 每次处理完3个账号的数据后,再进行下一天的任务
* @param day
*/
private void doOnceTasks(int day) {
int num=5;
List<Future> futureList = Lists.newArrayListWithCapacity(num);
for (int i = 0; i < num; ++i) {
int finalI = i;
Future future = cpuThreadPool.submit(() -> {
// 随机睡 0-5 秒
int sec = new Double(Math.random() * 5).intValue();
LockSupport.parkNanos(sec * 1000 * 1000 * 1000);
System.out.println(Thread.currentThread().getName() +"第" + day + "天的第 "+ finalI +"个下载 "+ " end");
});
futureList.add(future);
}
// 等待所有任务执行结束
for (Future future : futureList) {
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
执行下载任务时,出现 RejectedExecutionException 异常,默认情况下 ThreadPoolTaskExecutor 使用 AbortPolicy 拒绝策略,可以明确是因为向线程池提交了过多的任务。
原因分析:
java.util.concurrent.ThreadPoolExecutor#execute
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
//添加核心线程
if (addWorker(command, true))
return;
c = ctl.get();
}
//任务入阻塞队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
//线程池已被关闭 且 从阻塞队列中移除任务成功
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//执行到这里时,表示需要添加非核心线程了(参数false)
else if (!addWorker(command, false))
//非核心线程创建失败
reject(command);
}
调试上面的源码,Demo 进入的是第二个 reject 方法,我们继续跟踪,发现是因为线程池中的线程数量超过了 maximumPoolSize,所以执行了 reject 方法。
为什么任务入阻塞队列会失败 ?
查看源码可知,workqueue 在该业务中是 LinkedBlockingQueue( QueueCapacity > 0 ) ,所以我们需要去阅读 LinkedBlockingQueue 是如何处理提交和移除任务的。
java.util.concurrent.LinkedBlockingQueue#signalNotEmpty
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
// 加锁
takeLock.lock();
try {
// 唤醒一个 take 线程
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
可以看到,此处只是用 Condition 对象唤醒了一个空闲线程去执行任务,而不是所有线程。
我们阻塞队列只有 2,通过我们线程池的配置参数,每次批任务(5 个)必定提交一个到队列中。主线程不断的提交任务到线程池,线程池的队列异步的唤醒一个线程,并取一个任务。
所以我们可以知道,提交任务的速度 比 线程从阻塞队列取任务 的速度快,进而导致创建非核心线程执行任务,最终的结果就是:在多批任务之后,再无非核心线程可创建,导致执行拒绝策略。流程入下图所示:
解决方案:
本次问题是由 BlockingQueue 唤醒线程从阻塞队列取任务慢导致的,所以我们可以使用 SynchronousQueue 直接将任务提交给 等待的线程,避免出现这种情况。
思考:
1. Condition 对象是如何唤醒一个线程的?
Condition 是重入锁的伴生对象,它提供了在重入锁的基础上,进行等待(await())和通知(signal())的机制,更多相关的内容后期会附上链接。
2. 从业务和线程池参数来看,一定有线程是空闲的,为什么没使用到空闲线程来执行任务?而是执行了拒绝策略?
从 execute(Runnable command) 源码中我们可以看到,只有 BlockingQueue 会去唤醒空闲线程执行任务,而 BlockingQueue 每次只会唤醒一个线程,先等待分配资源,再去执行新任务,所以向线程池提交任务速率比唤醒线程执行任务速率大时,execute 方法中就会对新任务执行拒绝策略。