Java多线程执行单个大任务

目录

前言

一、方案分析

二、解决方案

三、总结

四、其他方案


前言

最近对一个接口执行进行优化,先简单说说接口的详情以及优化的方向吧。本文所提供的解决方法并不是一个好方案,但是本文的目的是在解决问题的同时,加深对线程池的了解与使用。

1、接口工作内容:

        该接口的主要工作内容是生成业务相关的图表(根据用户选择,可以生成1-n张图表),用户提交生成任务之后,便生成一个任务记录,初始状态为创建,用户将等待任务处理。任务状态有以下几种:创建、运行、失败、空数据、成功等,任务状态的变化将由任务执行情况而定。

2、接口现状:

        在用户提交生成任务之后,会将生成图表的任务提交到线程池中去执行。但是一个生成任务中,生成图表的数量不定,可为1-n张,如果都由线程池中的一个线程去执行,那么将会十分耗时!我这里的业务场景测试下来,生成7000多张的图表,将耗时两小时不止。。。

3、优化方向:

        由于一个线程去生成多张图表十分费时,于是做出以下改变:在线程池中的线程去执行多张图表生成任务时,首先是创建一个线程池,再将图表的生成任务提交至该线程池中,使得一个线程一次生成一张图表,如果在生成任意一张图表失败时,将视生成任务失败,则会关闭线程池,将图表生成任务标记为失败状态,否则,当所有图表都生成成功时,将图表生成任务标记为成功状态。

以上是这次多线程优化的相关内容,如果业务场景相同的话,可以耐心看看源代码,如果对你有帮助,那么我会很高兴的。

如果业务场景不同,但你又没有做过类似的业务,那么不妨静下心仔细看看,万一学到点东西了呢?是吧。


一、方案分析

        这里不少人会觉得为什么需要在线程中再创建线程池,为什么不能直接将每张图表的生成给到外层线程池中,这样就不用再创建线程池了?

        想法确实很美好,因为一个生成任务是需要生成n张图表的,而且任务的状态需要记录下来。如果把它们放到外层的线程池中去执行,那么,当有多个图表生成任务时,所有图表都混在一起生成,那么我怎么去记录不同图表生成任务的状态呢?这样我就不清楚哪个任务对应的所有图表生成成功,还是失败了。

        但是,如果我在一个线程中创建一个线程池,那么这个线程中的线程池,他只会去执行某个图表生成任务的所有图表生成,而不会去执行到其他任务的,在空间上就进行了隔离。而且可以很方便的去检测到任务的执行情况。

        这里会在每个线程执行时创建线程,所有图表生成结束后,也将会去关闭线程池。虽然频繁创建线程池和销毁线程池会有很大的开销,但是考虑到业务完成时间,以及实际接口的调用情况,边采用了当前这种方案。

二、解决方案

1、创建一个通用线程池,用于异步执行图表生成任务

@Configuration
public class ExecutorConfig {

    public static final int availableProcessors = Runtime.getRuntime().availableProcessors();
    public static final int queueCapacity = 100;
    public static final int keepAliveTime = 60;

    @Bean(name = "paintExecutor")
    public Executor paintExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(availableProcessors * 2);
        executor.setMaxPoolSize(availableProcessors * 4);
        executor.setQueueCapacity(queueCapacity);
        executor.setThreadNamePrefix("paint-service-");
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        executor.initialize();
        return executor;
    }

}

2、创建TaskVO,用于接收前端任务对象

@Data
public class TaskVO {

    // 任务唯一标识
    private String uuid;

    // 需要生成图表的个数,这里模拟用户生成多张,实际需要生成的勾选相关数据进行生成
    private int num;
    
    // 其他参数就是你创建任务选择的一些相关参数,这里不写了,根据你的需求自己改造
}

3、创建一个Controller层接口,用于图表生成任务调用,调用该接口,新增任务记录,异步提交图表生成任务

@RestController
@Slf4j
public class ChartController {

    @Autowired
    private ChartService chartService;

    @PostMapping("/createTask")
    public R<Boolean> createTask(@RequestBody TaskVO taskVO) {
        // 1、生成一个uuid,作为任务的唯一标识,后续根据uuid修改任务执行状态
        String uuid = IDUtil.getUUID();
        taskDTO.setUuid(uuid);


        // 2、记录该任务,将uuid,初始状态、参数等相关信息进行记录,后续需要查找该记录
        boolean add = chartService.addRecord(taskVO);

        // 3、异步执行图表生成任务,该方法加有@Async("paintExecutor")注解
        if(add){
            chartService.submitTask(taskVO);
        }

        // 4、根据步骤1,返回任务创建结果
        return new R<>(uuid, null, add);
    }

}

提交图表生成任务使用异步的目的是为了,用户在创建任务成功后,将立即返回任务,图表生成的过程将会后台异步完成。

3、创建service

public interface ChartService{

    /**
     * 任务中心新增记录
     * @param taskVO
     * @return
     */
    Boolean addRecord(TaskVO taskVO);

    /**
     * 1、异步执行图表生成任务
     * 2、等待图表任务执行结果
     * 3、修改任务状态以及其他处理
     * @date 2022/7/5
     */
    void submitTask(TaskVO taskVO);

}

4、创建实现类

@Service
@Slf4j
public class ChartServiceImpl implements ChartService{

    /**
     * 任务中心新增记录
     * @param taskVO
     * @return
     */
    @Override
    public Boolean addRecord(TaskVO taskVO){
        // 内容略,自行向数据库插入一条数据,todo
    }

    /**
     * 1、异步执行图表生成任务
     * 2、修改任务状态以及其他处理
     * @date 2022/7/5
     */
    @Async("paintExecutor")
    @Override
    public void submitTask(TaskVO taskVO){
        final int num = taskVO.getNum();
        if(num <= 0){
            return;
        }

        // 线程池创建
        int core = Runtime.getRuntime().availableProcessors() * 2; // 图表生成主要多为io,设置为处理器核数两倍
        int max = core; // 设置一样,避免线程抖动
        // 设置任务拒绝策略为:DiscardPolicy,作用,遇到线程池关闭或者任务过长,直接把队列中的任务丢弃
        final ExecutorService chartExecutorService = new ThreadPoolExecutor(core, max, 10,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(10000));

        // 计数器,用来计算任务执行结果
        final AtomicInteger counter = new AtomicInteger(0);
        // 锁,线程池关闭与向线程池提交任务同步,避免生成其中一张图出现异常进入线程池关闭期间与向线程池提交任务起冲突
        final Object shutdownAndSubmitLock = new Object();

        // 向图表线程池提交任务,生成num张图表
        log.info("开始生成图表, uuid=" + taskVO.getUuid());
        for(int i = 0 ;i < num; i++){
            // 同步,避免关闭线程池与向线程池提交任务之间出现并发问题,导致线程池关闭之后还在向线程池提交任务,可以不加锁,使用线程池的拒绝策略。
            // 任务提交由主线程加锁提交
            synchronized (shutdownAndSubmitLock){
                // 线程池没有被关闭,则提交任务
                if(!chartExecutorService.isShutdown()){
                    chartExecutorService.execute(() ->{
                        try{
                            // 生成报表,todo
                            //执行成功,计数器+1
                            counter.incrementAndGet();
                        }catch (Exception e){
                            e.printStackTrace();
                            log.error("uuid=" + taskVO.getUuid() + ",图表生成过程中出现异常,原因为:" + e.getMessage());

                            // 更新任务为失败状态,todo

                            // 同步,避免关闭线程池与向线程池提交任务之间出现并发问题,导致线程池关闭之后还在向线程池提交任务,可以不加锁,使用线程池的拒绝策略
                            // 关闭线程池由出现执行异常的异步线程关闭
                            synchronized (shutdownAndSubmitLock){
                                if(!chartExecutorService.isShutdown()){
                                    chartExecutorService.shutdownNow();
                                }
                            }
                        }finally {
                            // 所有图表生成完成
                            if(counter.get() == num){
                                log.info("图表生成结束, uuid=" + taskVO.getUuid());
                               
                                 // 更新任务为成功状态,todo

                                // 同步,避免关闭线程池与向线程池提交任务之间出现并发问题,导致线程池关闭之后还在向线程池提交任务,可以不加锁,使用线程池的拒绝策略
                                // 关闭线程池由执行最后一个线程关闭
                                synchronized (shutdownAndSubmitLock){
                                    if(!chartExecutorService.isShutdown()){
                                        chartExecutorService.shutdownNow();
                                    }
                                }
                            }
                        }
                    });
                }
            }
        }
    }

}

 线程池调用shutdownNow去关闭线程池后,后续提交或者在队列中等待的任务将不会执行到了。


三、总结

以上代码写的很复杂,确实不是一份好代码,但是以此作为记录,为下次问题的解决方案提供思路。如果有更好的方案,欢迎小伙伴分享给我。

四、其他方案

此方案与2023/8/15,进行补充

由于上述方案中我们执行任务的时候,每一个大任务都是创建一个线程池去执行,那么如果任务量多的话,将会导致我们服务中存在大量线程池,我们创建的本意就是将一个大任务分隔成多个小任务执行,最终还能得到结果。基于以上情况,可以参考下列方案进行修改,只需将线程池抽取出来作为公共的即可:

线程池执行FutureTask,得到返回结果_futuretask获取返回值_明天再去学习的博客-CSDN博客

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值