ThreadPoolExecutor避免调用者线程参与运算的几种方案
trank_南尘 2019-12-04 15:52:53 203 收藏
文章标签: 多线程 ThreadPoolExecutor
版权
问题
项目中用到了ThreadPoolExecutor,有一个需求场景是:不希望主线程(调用线程)参与计算。
先了解下一些前提,线程池的原理:
当提交任务时,如果当前线程池已创建的线程数小于核心线程数(corePoolSize),则创建线程去执行任务;
如果当前线程数达到了核心线程数,则将任务放入到阻塞队列(workQueue,一般用LinkedBlockingQueue);
如果阻塞队列也满了,并且当前线程数低于最大线程数(maximumPoolSize),则继续创建线程去执行任务;
如果阻塞队列满了,当前线程数达到了最大线程数,则触发拒绝策略(依赖初始化ThreadPoolExecutor时,选择哪种拒绝策略)
JUC提供了4种默认实现的拒绝策略,分别为:
CallerRunsPolicy:调用者线程参与执行(常用);
AbortPolicy:直接抛错;
DiscardPolicy:丢弃当前任务;
DiscardOldestPolicy:丢弃在队列中存在时间最长的任务(队头节点)。
当项目中有大量比较耗时的计算任务时,很容易想到去使用多线程提高效率,但同时又不想丢弃任何队列任务,否则业务数据会有问题,所以只能选择CallerRunsPolicy。因此初始化线程池大概如下:
private static final ThreadPoolExecutor REFRESH_THREAD_POOL = new ThreadPoolExecutor(
10,
maxPoolSize,
60 * 5L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(maxPoolSize),
new ThreadPoolExecutor.CallerRunsPolicy());
1
2
3
4
5
6
7
因为任务数量较大,当阻塞队列满了,且线程数达到了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);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
优点:最符合线程池分配原理的方案
缺点:需要主动sleep(休眠时间不太好确定),否则while(true)会很可怕,cpu飙高;
-
重写阻塞队列的offer方法(推荐)
查看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;
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
优点:简单,不需要考虑任务的数量
缺点:线程数量最多是corePoolSize,因为workQueue满了,任务的入队就会被阻塞住,也不会去创建更多的线程,建议corePoolSize=maximumPoolSize。
最终选择的是第三种方案。