项目中用到了ThreadPoolExecutor,有一个需求场景是:不希望主线程(调用线程)参与计算。
先了解下一些前提,线程池的原理:
JUC提供了4种默认实现的拒绝策略,分别为:
当项目中有大量比较耗时的计算任务时,很容易想到去使用多线程提高效率,但同时又不想丢弃任何队列任务,否则业务数据会有问题,所以只能选择CallerRunsPolicy。因此初始化线程池大概如下:
private static final ThreadPoolExecutor REFRESH_THREAD_POOL = new ThreadPoolExecutor(
10,
maxPoolSize,
60 * 5L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(maxPoolSize),
new ThreadPoolExecutor.CallerRunsPolicy());
因为任务数量较大,当阻塞队列满了,且线程数达到了maxPoolSize时,根据拒绝策略,调用者线程会去参与执行任务。
问题来了,因为调用者线程参与了计算,如果调用者线程运行的任务特别耗时(比其他子线程分配到的任务耗时大很多),那么所有的子线程执行完手上的任务,并且队列中的任务也被执行完之后,所有的子线程会全部阻塞去等待调用者线程分配任务,但是调用者线程还在执行任务,无法去分配任务给线程池,由此可见造成了很大的资源浪费,性能下降很多,只有当调用者线程执行完手中的任务才会回头去给线程池分配任务。(其实还有一点稍微影响性能的地方:当所有的子线程阻塞时,并且队列空了,超过核心线程数的线程会根据设置的存活时间被shutdown,等调用者线程分配任务后又需要重新创建一些线程,产生了一些没必要的系统开销)。
将阻塞的队列的设大,比如new LinkedBlockingQueue<>(5000)
优点:简单粗暴
缺点:workQueue队列可能会非常大,需要评估任务数量(任务量非常大会占用大量内存);线程池的数量最多是corePoolSize(如果超过则说明任务数量大于workQueue,不是本方法的目的了),建议corePoolSize=maximumPoolSize。
虽然JUC只提供了4种,但是我们可以实现它的接口,自定义拒绝策略:
public class CallerBlockedPolicy implements RejectedExecutionHandler {
/**
* 默认1000ms
* 休眠时间,防止轮循过于频繁
*
* 如果待执行的任务比较快,休眠时间设置小一些,否则可适当设长(建议:休眠时间小于任务的执行时间)
*/
private Long sleepTime;
public CallerBlockedPolicy() {
}
public CallerBlockedPolicy(Long sleepTime) {
this.sleepTime = sleepTime;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
//判断队列中能直接入队且不会阻塞的数量
while (e.getQueue().remainingCapacity() == 0) {
try {
//防止频繁轮询 主动休眠一小段时间
if (Objects.isNull(sleepTime)
|| sleepTime <= 0) {
sleepTime = 1000L;
}
Thread.sleep(sleepTime);
} catch (InterruptedException e1) {
//log it
}
}
e.execute(r);
}
}
}
优点:最符合线程池分配原理的方案
缺点:需要主动sleep(休眠时间不太好确定),否则while(true)会很可怕,cpu飙高;
查看execute()源码会发现,任务的入队是offer(), 这个方法不会阻塞,直接返回入队是否成功(boolean),改成使用put(),这是阻塞方法:
private static Integer maxPoolSize = 10;
private static final ThreadPoolExecutor THREAD_POOL = new ThreadPoolExecutor(
10,
maxPoolSize,
60 * 5L,
TimeUnit.SECONDS,
new LinkedBlockingQueue(maxPoolSize) {
@Override
public boolean offer(Runnable runnable) {
try {
super.put(runnable);
} catch (Exception e) {
//log it
}
return true;
}
});
优点:简单,不需要考虑任务的数量
缺点:线程数量最多是corePoolSize,因为workQueue满了,任务的入队就会被阻塞住,也不会去创建更多的线程,建议corePoolSize=maximumPoolSize。