ExecutorService.invokeAny()和ExecutorService.invokeAll()的源码阅读心得

ExecutorService.invokeAny()和ExecutorService.invokeAll()的使用方式,可以参考我之前的一篇文章。之前的博客按照test and learn的学习方式(有点类似于测试驱动开发),构造了我能想到的几种场景来进行测试,通过分析测试结果,也得出了一些结论,学会了这2个API的使用。本文主要是阅读下J.U.C源码,一则验证下之前的一些推论,二则学习下源码中的一些技巧,这是通过执行测试用例看不到的。

这2个API的实现逻辑是在AbstractExecutorService中

public abstract class AbstractExecutorService implements ExecutorService{
}

首先分析下invokeAny的源码

private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,boolean timed, long nanos)
	throws InterruptedException, ExecutionException, TimeoutException
{
   if (tasks == null)
       throw new NullPointerException();
   int ntasks = tasks.size();
   if (ntasks == 0)
       throw new IllegalArgumentException();
   List<Future<T>> futures= new ArrayList<Future<T>>(ntasks);
   ExecutorCompletionService<T> ecs =
       new ExecutorCompletionService<T>(this);

   // For efficiency, especially in executors with limited
   // parallelism, check to see if previously submitted tasks are
   // done before submitting more of them. This interleaving
   // plus the exception mechanics account for messiness of main
   // loop.

   try 
   {
       // Record exceptions so that if we fail to obtain any
       // result, we can throw the last exception we got.
       ExecutionException ee = null;
       long lastTime = (timed)? System.nanoTime() : 0;
       Iterator<? extends Callable<T>> it = tasks.iterator();

       // Start one task for sure; the rest incrementally
       futures.add(ecs.submit(it.next()));
       --ntasks;
       int active = 1;

       for (;;) {
           Future<T> f = ecs.poll();
           if (f == null) {
               if (ntasks > 0) {
                   --ntasks;
                   futures.add(ecs.submit(it.next()));
                   ++active;
               }
               else if (active == 0)
                   break;
               else if (timed) {
                   f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
                   if (f == null)
                       throw new TimeoutException();
                   long now = System.nanoTime();
                   nanos -= now - lastTime;
                   lastTime = now;
               }
               else
                   f = ecs.take();
           }
           if (f != null) {
               --active;
               try {
                   return f.get();
               } catch (InterruptedException ie) {
                   throw ie;
               } catch (ExecutionException eex) {
                   ee = eex;
               } catch (RuntimeException rex) {
                   ee = new ExecutionException(rex);
               }
           }
       }

       if (ee == null)
           ee = new ExecutionException();
       throw ee;
   } finally 
   {
       for (Future<T> f : futures)
           f.cancel(true);
   }
}


首先值得一提的是第13~17行的注释,这段注释主要是为了解释第28行代码(在开始main loop之前,先向线程池提交一个任务)

For efficiency, especially in executors with limited parallelism(平行/并行), 
check to see if previously submitted tasks are done before submitting more of them.
This interleaving(交叉/交错) plus the exception mechanics account for messiness(杂乱情况) of main loop.  

如果我们线程池中只有1个线程,那么提交到线程池中的任务是按照顺序串行执行的,即没有并发能力。一旦有一个任务正常完成,invokeAny就会返回这个任务的执行结果。所以先提交1个任务,让这个任务能够尽早执行,这种方式比一下子将所有任务都提交到线程池中效果要略好一些。如果线程池中有很多线程,这种先提交一个任务的方式,也没有什么坏处。


finally块中72~73代码功能也很简单,一旦有一个任务正常完成(执行过程中没有报异常),那么就会取消其他尚未完成的任务。这里我们不管任务是否完成,直接调用Future.cancel()方法进行取消。之所以可以这样做,是因为取消一个已经完成的任务不会带来什么负面影响,所以这里代码就简化了很多。这也提示我们:设计API的时候,要站在使用者的角度,考虑他们的调用习惯和使用场景,按照最容易使用,最合乎逻辑的原则


现在我们来看下最复杂的main loop,主要的逻辑都是在这个for循环中完成的。首先要明白变量ntasks和active的含义,ntasks表示当前还有几个任务没有提交;active表示已经提交到线程池中但是还没有执行完成的任务数。当ntasks减少到0的时候,意味着所有任务都已经提交到线程池;当active减少到0的时候,意味着所有任务已经执行完毕。第36~38行:每当向线程池中提交一个新任务,ntasks--,active++;第53~54行:每当有任务执行完成,active--。


第33行:如果有任务执行完成(正常返回或者异常退出),那么返回任务对应的Future;如果没有任务完成,则返回null。关于CompletionService的使用,可以参考这篇文章


53~64行:如果第33行代码返回值不是null,这意味着已经有任务执行完成。--active意味着有一个任务已经完成,从线程池中退出了。第55~63行,我们通过future.get()获取任务的返回结果。

1、如果该方法没有报异常,那么意味着任务正常完成,会先执行finally块取消线程池中未完成的任务,然后invokeAny()返回该结果。

2、如果该方法抛出了InterruptedException,那么意味着当前调用invokeAny的线程被中断,这个时候invokeAny()抛出InterruptedException异常。

3、如果该方法抛出了ExecutionException或者RuntimeException,那么意味着当前任务是异常结束的。按照invokeAny的语义,invokeAny返回的是最先正常完成任务的直接结果。所以还需要继续进行for循环,看看其他尚未结束的任务是否会正常完成。


第34~52行:如果33行代码返回值是null,意味着当前还没有任务执行完成。第35~39行,如果还有任务没有提交到线程池(ntasks>0),那么将当前任务提交到线程池中执行。第40~41行:如果所有任务已经执行完成(active == 0) ,那么退出for循环,进入67~69行说明所有任务都是异常结束的,最后invokeAny抛出ExecutionException。第42~49行:程序走到这里(ntasks==0 && active!=0),这意味着任务已经全部提交到了线程池。如果invokeAny设置了超时时间,则进入43~48行;如果是不限时版本的invokeAny,则进入第51行。


第43~48行:如果ecs.poll(nanos, TimeUnit.NANOSECONDS)返回的是null,这意味着超时期已满,但还是没有任务完成,invokeAny抛出TimeoutException。如果ecs.poll(nanos, TimeUnit.NANOSECONDS)返回的不是null,说明在超时之前已经有任务完成,那么46~48行需要扣除当前已经流逝的时间。这里不需要判断剩余时间nanos是否小于0,因为在J.U.C中如果超时时间小于或等于0,代表API不等待,立刻返回。


至此,invokeAny源码分析完毕,函数的表现也的确与我们测试的结果一致。可以看到,如此多的if分支,很容易遗漏,所以编写并发代码是一件很困难的事,尤其是在编写底层类库的时候。接下来我们看下不限时版本的invokeAll。

public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
  throws InterruptedException 
{
  if (tasks == null)
      throw new NullPointerException();
  List<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
  boolean done = false;
  try {
      for (Callable<T> t : tasks) {
          RunnableFuture<T> f = newTaskFor(t);
          futures.add(f);
          execute(f);
      }
      for (Future<T> f : futures) {
          if (!f.isDone()) {
              try {
                  f.get();
              } catch (CancellationException ignore) {
              } catch (ExecutionException ignore) {
              }
          }
      }
      done = true;
      return futures;
  } finally {
      if (!done)
          for (Future<T> f : futures)
              f.cancel(true);
  }
}
  

这段代码总体来说很简单,也比较容易想到。需要注意的是14~22行的代码,这个for循环的目的就是阻塞调用invokeAll的线程,直到所有任务都执行完毕;当然我们也可以使用别的方式来实现阻塞,不过这种方式是最简单的。如果f.isDone()返回false,就意味着当前future还没有结束,调用f.get让线程陷入阻塞。这里没有忽略InterruptedException,就是为了当调用invokeAll()的线程被中断的时候,能够响应中断。


下面看下限时版本的invokeAll()

public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                         long timeout, TimeUnit unit)
        throws InterruptedException 
{
    if (tasks == null || unit == null)
        throw new NullPointerException();
    long nanos = unit.toNanos(timeout);
    List<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
    boolean done = false;
    try {
        for (Callable<T> t : tasks)
            futures.add(newTaskFor(t));

        long lastTime = System.nanoTime();

        // Interleave time checks and calls to execute in case
        // executor doesn't have any/much parallelism.
        Iterator<Future<T>> it = futures.iterator();
        while (it.hasNext()) {
            execute((Runnable)(it.next()));
            long now = System.nanoTime();
            nanos -= now - lastTime;
            lastTime = now;
            if (nanos <= 0)
                return futures;
        }

        for (Future<T> f : futures) {
            if (!f.isDone()) {
                if (nanos <= 0)
                    return futures;
                try {
                    f.get(nanos, TimeUnit.NANOSECONDS);
                } catch (CancellationException ignore) {
                } catch (ExecutionException ignore) {
                } catch (TimeoutException toe) {
                    return futures;
                }
                long now = System.nanoTime();
                nanos -= now - lastTime;
                lastTime = now;
            }
        }
        done = true;
        return futures;
    } finally {
        if (!done)
            for (Future<T> f : futures)
                f.cancel(true);
    }
}

这段代码也比较简单,需要说明的是2处 if (nanos <= 0)的判断。

第24~25行:每当向线程池提交一个任务之后,就判断是否超时。这样的话,如果在任务还没有全部提交到线程池中,用户设置的超时期已满,那么就能够快速检查出这种情况。

第30~31行:每次在调用f.get(nanos, TimeUnit.NANOSECONDS)进入阻塞状态之前,先做下是否超时期满的检查。这里的目的其实也是想能快速检查超时期是否已满。

还有就是28~43行这个for循环,如果当前当前任务的future.isDone()返回true,代表任务执行完成,这个时候是不会进行超时检查的。考虑这种情况:

如果向线程池中提交了5个任务,在28行代码for (Future<T> f : futures)开始之前之前,task1,task2,task3已经执行完成(f.isDone()返回true),task4和task5还在执行中,加入此时用户设置的超时期已满。那么至少要等到循环到task4,才能检测出已经超时。也是说:实际发生超时期满,和程序探测到超时满,是存在延时的。不过好在这个延时一般很短,而且我们使用限时版本的invokeAll(),也不会将超时期设置的很短。


你可能感兴趣的:(java线程池源码)