利用多线程实现报表的高效导出

时间:2024-01-23 22:48:56

多线程、线程池、并发包每当谈起这些词汇,可能不是在面试就是在准备面试的路上了。

有句话叫“面试造航母,工作拧螺丝“,确实很多情况下我们是用不到这些东西的,但是学好这些东西对我们的日常工作也可能会产生意想不到的好处的。

临近年末,收拾了下手头工作,趁着最后两天有些闲暇,准备着手优化下前段时间业务人员反馈的部分报表导出速度过慢的问题。

报表的优化主要是涉及两个方面,一个是SQL和数据库层面的优化,另一个就是代码层面的优化了,本文主要讲述代码层面利用多线程处理的一点小总结。

多线程实现的基础知识

实现多线程的方式

  • 继承Thread类创建线程
  • 实现Runnable接口创建线程
  • 实现Callable接口创建线程
  • 线程池的实现

JDK自带的五种线程池的使用场景

  • newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。

  • newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。

  • newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。

  • newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。

  • newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。

 如何自定义线程池

在实际的使用过程中,一般我们都是用Executors去创建线程池,如果有一些其他的需求,比如指定线程池的拒绝策略,阻塞队列的类型,线程名称的前缀等等,我们可以采用自定义线程池的方式来解决。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) ;
  • corePoolSize:线程池大小,决定着新提交的任务是新开线程去执行还是放到任务队列中,也是线程池的最最核心的参数。一般线程池开始时是没有线程的,只有当任务来了并且线程数量小于corePoolSize才会创建线程。

  • maximumPoolSize:最大线程数,线程池能创建的最大线程数量。

  • keepAliveTime:在线程数量超过corePoolSize后,多余空闲线程的最大存活时间。

  • unit:时间单位

  • workQueue:存放来不及处理的任务的队列,是一个BlockingQueue。

  • threadFactory:生产线程的工厂类,可以定义线程名,优先级等。

  • handler:拒绝策略,当任务来不及处理的时候,如何处理, 前面有讲解。

execute和submit的区别

  • execute适用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了
  • submit方法适用于需要关注返回值的场景,在线程执行结束会返回响应的结果值

其实这两种方法的底层就是Runnable,Callable的实现。

多线程的一些基础小知识,有兴趣的同学可以园子里翻翻其他同学的介绍,多线程、线程池、并发包这些东西无论是学习还是面试都是比较重要的。

报表优化案例

报表导出慢的原因探查

仔细检查了需要优化的报表,发现因为这个报表的实时性要求比较高,同时涉及大量数据的计算操作,在优化了sql后效率还是无法达到满意的程度,所以决定采用多线程的方式多个线程同时处理不同的业务逻辑,最后在合并数据返回,以达到提高效率的目的。

代码解决方案

初步决定采用ExecutorService的submit方法,将一个复杂报表拆分为四个子线程执行并返回结果。同时采用并发包中的CountDownLatch做同步器,等待 四个子线程执行完毕后,再在主线程进行数据合并操作。假如每个子线程的执行时长在10分钟左右,如果采用原先的串行方式的话,四个业务处理大概需要40分钟左右,现在这种并行的方式执行只需要十分钟的处理时间。

伪代码实现

        long startTime = DateUtils.getCurrentDateTime().getTime();
        ExecutorService service = Executors.newFixedThreadPool(4);
        CountDownLatch latch = new CountDownLatch(4);
        Future<List<CapitalVO>> borrowIncrement = service.submit(new Callable<List<CapitalVO>>() {
            @Override
            public List<CapitalVO> call() throws Exception {

                List<CapitalVO> list = listBorrowIncrement(startDate, endDate);
                latch.countDown();
                return list;
            }
        });
        Future<List<OwnVO>> beceiveAccount = service.submit(new Callable<List<OwnVO>>() {
            @Override
            public List<OwnVO> call() throws Exception {

                List<OwnVO> list = listReceiveAccount(startDate, endDate);
                latch.countDown();
                return list;
            }
        });
        Future<List<OwnVO>> buaranteeAccount = service.submit(new Callable<List<OwnVO>>() {
            @Override
            public List<OwnVO> call() throws Exception {
                List<OwnVO> list = listGuaranteeAccount(startDate, endDate);
                latch.countDown();
                return list;
            }
        });
        Future<List<BorrowerVO>> borrowerRepayment = service.submit(new Callable<List<BorrowerVO>>() {
            @Override
            public List<BorrowerVO> call() throws Exception {
                List<BorrowerVO> list = listBorrowerRepayment(startDate, endDate);
                latch.countDown();
                return list;
            }
        });
            latch.await();
            List<CapitalVO> borrowCapitalIncrement = borrowIncrement.get();
            List<OwnVO> ownReceive = beceiveAccount.get();
            List<OwnVO> ownAccountGuan = buaranteeAccount.get();
            List<BorrowerVO> borrower = borrowerRepayment.get();

上述代码利用CountDownLatch实现了线程同步,同时解决了原本串行执行时间较长的问题,在最终的效果上也是达到了预期的优化目标,比原报表的处理时长减少了四分之三的时间。

另外,有同学提出现在是实现了四个线程并行处理,处理时长大概在十分钟左右。但是假如其中一个线程出现了报错,不在需要其他线程继续执行,这个时候该怎么处理呢?

确实是存在这个情况的,其实我们可以利用Future对象的 cancel(boolean mayInterruptIfRunning)来中断其他线程,底层其实还是thread.interrupt()的方法实现。

总结

总的来说技术方案上并没有什么特别的东西,但是有时候有没有往这方面做就是一个思考的问题了。其实在工作中九成以上的人每天都是在做CRUD的业务,但是即便是CRUD每个人做出来的东西还是有所不同的。多思考多实践,其实多线程并没有那么遥不可及,即便是简单的报表,也是可以做出不一样的东西的。

最后,新年临近,祝福大家新年快乐,也希望自己能够在新的一年做一个合格的creative worker。