ExecutorService.invokeAny()和ExecutorService.invokeAll()的使用方式,可以参考我之前的一篇文章。之前的博客按照test and learn的学习方式(有点类似于测试驱动开发),构造了我能想到的几种场景来进行测试,通过分析测试结果,也得出了一些结论,学会了这2个API的使用。本文主要是阅读下J.U.C源码,一则验证下之前的一些推论,二则学习下源码中的一些技巧,这是通过执行测试用例看不到的。
这2个API的实现逻辑是在AbstractExecutorService中
public abstract class AbstractExecutorService implements ExecutorService{ }
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(),也不会将超时期设置的很短。