ExecutorCompletionService源码学习

时间:2022-07-23
本文章向大家介绍ExecutorCompletionService源码学习,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

之前在项目中使用过ExecutorCompletionService,当时并不知道这个类的具体原理。希里糊肚的就用了,然后实现了功能。现在算是对之前的使用疑问进行一次解析吧。ExecutorCompletionService是一种对类似多线程任务调度器存在。之前说ThreadPoolExecutor是多线程执行器。

那么ExecutorCompletionService就是用来调度任务和线程执行器ThreadPoolExecutor的。之前我们通过Thread和ThreadPoolExecutor的学习知道实现runnable接口测试任务的核心。而ThreadPoolExecutor则是线程的执行容器,用来管理线程的。因此通过runnable接口的代理作用可以做一些事情,比如针对不同的任务

采用不同的调用方式。而ExecutorCompletionService就是这样实现的。那么我们就详细学习一下ExecutorCompletionService类的实现机理吧!

我们看到ExecutorCompletionService代码量不多,其中的submit肯定是提交任务的。我们还是按照类的初始化和基本调用方法的步骤来学习其实现过程吧。


    //这里的参数是Executor,也就是线程池,因为此类是一个调度器。
    public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?(AbstractExecutorService) executor : null;
        //初始化一个阻塞队列,这个是做什么的?先打个疑问
        this.completionQueue = new LinkedBlockingQueue<Future<V>>();
    }
   //传入线程池执行器,和队列
    public ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue) {
        if (executor == null || completionQueue == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?(AbstractExecutorService) executor : null;
        this.completionQueue = completionQueue;
    }

在任务提交的时候

//提交一个callable任务。因为callable也是实现了runnable接口,所以可以代理
  public Future<V> submit(Callable<V> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task);
        executor.execute(new QueueingFuture(f));
        return f;
    }


    public Future<V> submit(Runnable task, V result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task, result);
        //让线程池执行器来执行execute方法,传入的是代理的Future任务,也就是最终调用的是QueueingFuture的run方法。
        executor.execute(new QueueingFuture(f));
        return f;
    }


    private RunnableFuture<V> newTaskFor(Runnable task, V result) {
        if (aes == null)
      //创建一个futureTask任务
            return new FutureTask<V>(task, result);
        else
          //添加一个任务
            return aes.newTaskFor(task, result);
    }


在FutureTask的run方法中对线程执行完毕的结果进行了处理。
    public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                   //调用了call方法,也就是我们要写的业务模块。
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    //将返回的结果设置FuretuTask这个类中的Object中
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

分析到这里的话,我们就很容易的理解前边的那个疑问了。this.completionQueue = new LinkedBlockingQueue<Future<V>>();显然就是为了缓存我们的任务的,因为在调用FatureTask之后会有返回值,返回值在任务中的Object属性中,但是提交的任务又有很多,所以用阻塞队列缓存下来保证有序性,然后方便后续获取。

那么获取的逻辑又是怎么样的呐?我们来看看。

    public Future<V> take() throws InterruptedException {
        return completionQueue.take();
    }

这里的take方法显然是获取阻塞的任务队列的头节点也就是目前的第一个任务。拿到任务之后我们要获取其中的Object值,就要调用FutureTask中的get()获取当前任务的返回值。

最后我有点小疑问就是,我们知道我们的线程池执行器其实也是有任务队列的,现在好了ExecutorCompletionService自己又有任务队列。这两者的容量大小有限制,会不会因为线程池的任务队列太小导致两者有点小问题。仔细分析就知道这是没问题的,这里的队列是正对运行时的,而线程池的阻塞队列是发生在之前的。所以两者不冲突。好了源码就分析到这里。