ExecutorCompletionService业务场景
今天在维护代码时,看见架构师优化过查询,让查询并发的去查,并且用的ExecutorCompletionService类,推送git时说是提速了2s。
ExecutorCompletionService介绍
当我们向Executor提交一组任务,并且希望任务在完成后获得结果,此时可以考虑使用ExecutorCompletionService。
ExecutorCompletionService实现了CompletionService接口。ExecutorCompletionService将Executor和BlockingQueue功能融合在一起,使用它可以提交我们的Callable任务。这个任务委托给Executor执行,可以使用ExecutorCompletionService对象的take和poll方法获取结果。
ExecutorCompletionService的设计目的在于提供一个可获取线程池执行结果的功能,这个类采用了装饰器模式,需要用户提供一个自定义的线程池,在ExecutorCompletionService内部持有该线程池进行线程执行,在原有的线程池功能基础上装饰额外的功能。
下面是ExecutorCompletionService的原理图:
1、在使用ExecutorCompletionService时需要提供一个自定义的线程池Executor,构造ExecutorCompletionService。同时,也可以指定一个自定义的队列作为线程执行结果的容器,当线程执行完成时,通过重写FutureTask#done()将结果压入队列中。
2、当用户把所有的任务都提交了以后,可通过ExecutorCompletionService#poll方法来弹出已完成的结果,这样做的好处是可以节省获取完成结果的时间。
下面是使用队列和不使用队列的流程对比,从图中我们可以看出,在使用队列的场景下,我们可以优先获取到完成的线程,当我们要汇总所有的执行结果时,这无疑会缩减我们的汇总时间。
而不使用队列时,我们需要对FutureTask进行遍历,因为我们不知道哪个线程先执行完了,只能挨个去获取结果,这样已经完成的线程会因为前面未完成的线程的耗时而无法提前进行汇总。
如果算上汇总结果的耗时时间:
在使用队列的场景下,我们可以在其他任务线程执行的过程中汇总已完成的结果,节省汇总时间。不使用队列的场景下,只用等到当前的线程执行完成才能汇总。
ExecutorCompletionService中take和poll有啥区别?
take()
和poll()
都是ExecutorCompletionService
的方法,用于从阻塞队列中获取已完成的Future
任务的结果。
take()
是一个阻塞方法,如果队列中没有已完成的任务,则该方法会一直等待,直到有一个任务完成并将其结果添加到队列中,然后返回该结果。如果队列中已经有一个或多个已完成的任务,则该方法会返回其中最先完成的任务的结果。
poll()
也是用于获取已完成的任务结果的方法,但它是非阻塞的。如果队列中没有已完成的任务,则该方法立即返回null
。如果队列中有一个或多个已完成的任务,则该方法返回其中最先完成的任务的结果。
因此,如果您需要立即获取结果并且不希望在队列中等待任务完成,可以使用poll()
方法。如果您想要等待直到有一个任务完成并返回其结果,或者希望确保先获取已完成的任务的结果,则可以使用take()
方法。
需要注意的是,如果队列中没有已完成的任务,则take()
方法将一直阻塞,直到有任务完成。这可能会导致您的程序在等待任务完成时长时间挂起,因此您需要仔细考虑使用take()
方法的场景。
ExecutorCompletionService源码分析
ExecutorCompletionService有三个私有属性,分别是executor、aes和completionQueue,其中completionQueue就是存储已完成任务的队列,具体代码如下图。
进入它的构造方法,在方法内部给它的三个属性赋值,可以看到在这里初始化了一个LinkedBlockingQueue类型的先进先出阻塞队列,具体代码如下图。
无论有参构造与无参构造,本质都是给上面三个私有属性赋值。
接着,进入ExecutorCompletionService的submit方法。
跟踪代码进入newTaskFor方法,具体代码如下图。
在ExecutorCompletionService构造方法中已经给aes赋过值了,所以进入AbstractExecutorService的newTaskFor方法,具体代码如下图。
跟踪代码进入FutureTask构造方法,具体代码如下图。
到这里构建的RunnableFuture实例对象完成了,回到上述的submit方法中,继续分析executor.execute(new QueueingFuture(f)),首先是new QueueingFuture(f),QueueingFuture是ExecutorCompletionService中的内部类,具体代码如下图。
从图中的代码可以看到,将RunnableFuture实例对象赋值给了QueueingFuture的task属性,注意上图红框中有一个done方法,它的内部是将一个task添加到已完成阻塞队列中,这个先记住后面会用到。接着,分析executor.execute(new QueueingFuture(f)),因为我们的实例演示代码中使用到的是ThreadPoolExecutor,所以executor.execute()方法执行到ThreadPoolExecutor中,具体重点代码如下图。
这里我们不分析极端的情况,当工作线程数小于核心线程数的时候,执行addWorker方法,这个方法体的内容比较多,这里只关注重点代码,具体代码如下图。
第一个红框中的代码会构建一个Worker实例,具体代码如下图。
根据上图中的红框代码,继续跟踪代码,会发现t.start()方法会执行到上图的run方法中,而run方法的内部执行了runWorker方法,具体代码如下图。
上图中代码继续跟踪可以发现,执行task.run()会进入前面构建的RunnableFuture实例对象FutureTask的run方法中,具体代码如下图。
第一个红框中的代码就是实际任务执行的代码,也就是submit提交的任务真正执行的地方。第二个红框中的代码是当发生异常时的处理,第三个红框中的代码是正常执行完成的处理
从上面两张图中的代码发现,都执行了finishCompletion()方法,下面来揭晓这个方法的作用,具体代码如下图。
从上图红框中的代码可以看到,这里执行了done()方法,实际执行的是我们前面分析提到的将一个task添加到已完成阻塞队列中的那个done方法。至此,当一个任务执行完成或异常的时候,都会被添加到已完成阻塞队列中,进而被取出处理。
下面再分析一下ExecutorCompletionService中的take方法和poll方法,具体代码如下图。
从上图可以看到,都是操作已完成阻塞队列,那我们就看一下这个已完成阻塞列队中的代码,如下图。
上图清晰的展示了通过循环等待已完成的执行任务。
上图代码不阻塞,当没有已完成的执行任务时,直接返回null。