2021-05-31

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,等调用者线程分配任务后又需要重新创建一些线程,产生了一些没必要的系统开销)。

解决方法:

  1. 调大阻塞队列(不推荐):
    将阻塞的队列的设大,比如new LinkedBlockingQueue<>(5000)
    优点:简单粗暴
    缺点:workQueue队列可能会非常大,需要评估任务数量(任务量非常大会占用大量内存);线程池的数量最多是corePoolSize(如果超过则说明任务数量大于workQueue,不是本方法的目的了),建议corePoolSize=maximumPoolSize。

  2. 自定义拒绝策略
    虽然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飙高;

  1. 重写阻塞队列的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。

最终选择的是第三种方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值