文章目录
- 如果下面的问题你都会的话就别在这浪费时间啦
- 1、invokeAll
-
- 1.1、源码
- 1.2、要点总结
- 1.3、Demo
-
- 1.3.1、代码
- 1.3.2、结果
- 1.3.3、分析
- 2、invokeAll(timeout)
-
- 2.1、源码
- 2.2、要点总结
-
- 2.2.1、核心流程
- 2.2.2、答疑环节
- 2.3、Demo
-
- 2.3.1、代码
- 2.3.2、结果
- 2.3.3、分析
- 3、invokeAny
-
- 3.1、源码
- 3.2、要点总结
-
- 3.2.1、核心流程
- 3.2.2、答疑环节
- 3.3、Demo
-
- 3.3.1、代码
- 3.3.2、结果
- 3.3.3、分析
- 4、从中学到了什么思想?
- 比如滴滴打车,你同时下单给快车、滴滴打车、优享、礼橙专车,肯定是多线程异步去执行的任务,其中一个接单后就自动取消其他车型的派单,怎么实现? (invokeAny)
- 线程池里的invokeAny和invokeAll啥区别?
- invokeAll原理是啥?用到了哪种设计模式?(模板模式)
- invokeAll怎么取消的任务执行?(interrupt)中途报错是取消所有任务执行吗?
- invokeAny的ExecutorCompletionService采取了什么设计模式?(装饰者模式)
1.1、源码
// 隶属于下面这个类 // java.util.concurrent.AbstractExecutorService#invokeAll(java.util.Collection<? extends java.util.concurrent.Callable<T>>) /** * tasks:需要批量执行的任务集合。 * 用于批量执行任务,并且将结果按照task列表中的顺序返回 */ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException { // 1.判空,安全校验 if (tasks == null) throw new NullPointerException(); // 2.将任务转成Future,结果集 ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size()); // 3.标识任务是否完成 boolean done = false; try { // 4.将任务转成Future,因为Future能获取返回值和取消的操作 for (Callable<T> t : tasks) { // 4.1 将Callable包装成RunnableFuture RunnableFuture<T> f = newTaskFor(t); // 4.2 添加到结果集 futures.add(f); // 4.3 !!!模板设计模式,交由子类处理具体的执行任务逻辑 execute(f); } // 5. 获取每个任务的执行结果,忽略部分异常 for (int i = 0, size = futures.size(); i < size; i++) { // 5.1 取出每一个Future,获取执行结果 Future<T> f = futures.get(i); // 5.2 如果当前任务没执行完的话,则进行等待。阻塞等待执行完成后才会进入下一次循环 if (!f.isDone()) { try { // 5.3 获取执行结果,若没执行完毕的话,则会阻塞等待,这里用意理解成只是阻塞等待,不关心返回结果,只要保证任务执行完成即可进入下一次for循环,FutureTask的get方法的源码有兴趣的可以看看 f.get(); } catch (CancellationException ignore) { // 忽略CancellationException异常 } catch (ExecutionException ignore) { // 忽略ExecutionException异常 } } } // 标记执行完成 done = true; return futures; } finally { // 6.若任务执行失败(抛出了忽略的两种异常之外的异常),则进行取消每个任务 if (!done) for (int i = 0, size = futures.size(); i < size; i++) // 6.1 逐个取消任务,点进去看源码最终是interrupt进行中断 futures.get(i).cancel(true); } }
1.2、要点总结
-
方法含义:用于批量执行任务,并且将结果按照task列表中的顺序返回
-
将Callable转成FutureTask且放到List中,目的是FutureTask可以取消任务也可以拿到返回结果
-
采取模板设计模式来完成任务的具体执行(execute方法)
-
循环遍历每一个任务,如果中途某个任务没执行完,则调用get方法阻塞等待,一直等待执行完毕后才会进入下一次for循环来执行其他任务
-
忽略CancellationException和ExecutionException两个异常
-
若任务执行失败(抛出了忽略的两种异常之外的异常),则进行取消每个任务(cancel方法)
5.1. 是真的取消吗?no!前面执行完成的无法收回,只是取消当前出错的以及后面未执行完成的任务,取消的策略就是interrupt,中断
1.3、Demo
四个任务批量执行,第二个任务报错了,那么会发生什么情况?
1.3.1、代码
public class AbstractExecutorServiceTest { public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(2); List<CallableTest1> callableList = new ArrayList(); callableList.add(new CallableTest1(1)); callableList.add(new CallableTest1(2)); callableList.add(new CallableTest1(3)); callableList.add(new CallableTest1(4)); List<Future<Integer>> futures = executorService.invokeAll(callableList); for (Future<Integer> f : futures) { System.out.println(f.get()); } } } class CallableTest1 implements Callable<Integer> { private int n; public CallableTest1 (int n) { this.n = n; } @Override public Integer call() { System.out.println("start: " + n); if (n == 2) return 1 / 0; return n; } }
1.3.2、结果
start: 1 start: 4 start: 2 start: 3 1 Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
1.3.3、分析
可以看到四个任务都得到了执行(start1 2 3 4都输出了,代表四个任务都进来了,这是由上面模板方法调用execute来执行的),但是到n=2的时候抛出了异常,且2之后的任务都没有输出。是因为出错了,调用了cancel方法逐个取消,**取消的是当前报错的任务后面排队未被执行的任务。**所以任务3和任务4未得到执行。而任务1是在报错之前执行完成的,所以cancel对其无效。
2、invokeAll(timeout)2.1、源码
// 隶属于下面这个类 // java.util.concurrent.AbstractExecutorService#invokeAll(java.util.Collection<? extends java.util.concurrent.Callable<T>>, long, java.util.concurrent.TimeUnit) /** * tasks:需要批量执行的任务集合。 * timeout:超时时间 * unit:超时时间单位 * * 用于批量执行任务,并且将结果按照task列表中的顺序返回,可设置这批任务总的执行超时时间。 */ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException { // 1.判空,安全校验 if (tasks == null) throw new NullPointerException(); // 2.先将超时时间转换为纳秒,注意是纳秒,所以要求超时时间很精确,纳秒级别的。 long nanos = unit.toNanos(timeout); // 3.将任务转成Future,结果集 ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size()); // 4.是否完成任务的标识 boolean done = false; try { // 5.将Callable都包装成future,因为Future能获取返回值和取消的操作 for (Callable<T> t : tasks) futures.add(newTaskFor(t)); // 6.重新设置超时时间,也就是说上面将Callable包装成Future这个for操作不能算到超时时间里去, // 也就是需要从execute开始算超时时间,所以真正的deadline(超时时间)是你设置的timeout+上面那一坨无用代码所消耗的纳秒。 final long deadline = System.nanoTime() + nanos; // 个人认为作者追求极致的话,这句话应该放到deadline上面,因为size()方法也会消耗时间。 final int size = futures.size(); // 7.模板设计模式,执行任务,具体怎么执行的,交给子类。 for (int i = 0; i < size; i++) { // 7.1 模板设计模式,执行任务 execute((Runnable)futures.get(i)); // 7.2 重新计算超时时间,用deadline - 当前系统纳秒值 // 也就是说减去调用execute所消耗的时间(execute是异步的,不会等具体任务执行完才返回哦) nanos = deadline - System.nanoTime(); // 7.3 如果调用execute这个异步api都超时了,那直接结束了,后面的任务得不到执行。 // 比如 超时时间设置贼笑,任务贼多,这个for里面还没全部调用完execute呢就超时了,那对不起,后面没被execute的任务就cancel了。 if (nanos <= 0L) return futures; } // 8. 获取每个任务的执行结果,忽略部分异常 for (int i = 0; i < size; i++) { // 8.1 取出每一个Future,获取执行结果 Future<T> f = futures.get(i); // 8.2 如果当前任务没执行完的话,则进行等待。阻塞等待执行完成后才会进入下一次循环 if (!f.isDone()) { // 8.3 若超过设置的执行时间,则返回结果,然后走到finally取消后面的任务 if (nanos <= 0L) return futures; try { // 8.4 获取执行结果,若没执行完毕的话,则会阻塞等待,这里用意理解成只是阻塞等待,不关心返回结果,只要保证任务执行完成即可进入下一次for循环,等待超过超时时间的话会抛出TimeoutException,然后被catch捕获到 return回去,FutureTask的get方法的源码有兴趣的可以看看 f.get(nanos, TimeUnit.NANOSECONDS); } catch (CancellationException ignore) { } catch (ExecutionException ignore) { } catch (TimeoutException toe) { return futures; } // 8.5 重新计算超时时间, deadline减去任务执行完成所消耗的时间。用于判断是否超时。 nanos = deadline - System.nanoTime(); } } done = true; return futures; } finally { // 若任务执行失败(抛出了忽略的两种异常之外的异常),则进行取消每个任务 if (!done) for (int i = 0, size = futures.size(); i < size; i++) // 逐个取消任务,点进去看源码最终是interrupt进行中断 futures.get(i).cancel(true); } }
2.2、要点总结
2.2.1、核心流程
-
方法含义:用于批量执行任务,并且将结果按照task列表中的顺序返回,可设置这批任务总的执行超时时间。
-
将Callable转成FutureTask且放到List中,目的是FutureTask可以取消任务也可以拿到返回结果
-
采取模板设计模式来完成任务的具体执行(execute方法)
-
循环遍历每一个任务,如果中途某个任务没执行完,则调用get方法阻塞等待,一直等待执行完毕后才会进入下一次for循环来执行其他任务,若或者等待超时的话会抛出TimeoutException,被catch住,直接返回任务数组
-
忽略CancellationException和ExecutionException两个异常,遇到TimeoutException的话会直接return任务数组
-
若任务执行失败(抛出了忽略的两种异常之外的异常),则进行取消每个任务(cancel方法)
5.1. 是真的取消吗?no!前面执行完成的无法收回,只是取消当前出错的以及后面未执行完成的任务,取消的策略就是interrupt,中断
2.2.2、答疑环节
- 为什么添加任务和execute任务要分成两个for?
因为为了计算超时时间的准确性,添加任务不算做超时时间,因为并未得到执行,只是准备工作而已,所以真正的超时时间需要从execute开始算起。所以弄了两个for。
- 为什么execute那块要判断超时时间nanos,而下面获取结果的时候不是用nanos去减当前时间,而是继续用原来的deadline?
两次时间各有不同的含义,一个是假设任务上万个,还没放完放任务(也就是调用execute方法)就超时了,那就直接return,另一个是任务放完了,但是执行每个任务所消耗的时间超时了。两个时间都是独立互不影响的,每次都是用deadline重新去减,也就是说execute放任务所消耗的时间如果没超时的话,下面任务具体执行所消耗的时间会重新计算,不会把放任务(调用execute方法)消耗的时间也算在内。
2.3、Demo
2个线程批量执行4个任务,要求批量执行的总耗时不能超过2s,每个任务需要耗时1.5s,也就是说只能处理两个任务,在处理第三个任务过程中就会超时,那么会发生什么情况?
2.3.1、代码
public class AbstractExecutorServiceTest { public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(2); List<CallableTest2> callableList = new ArrayList(); callableList.add(new CallableTest2(1)); callableList.add(new CallableTest2(2)); callableList.add(new CallableTest2(3)); callableList.add(new CallableTest2(4)); List<Future<Integer>> futures = executorService.invokeAll(callableList, 2, TimeUnit.SECONDS); for (int i = 0; i < futures.size(); i ++) { Future<Integer> f = futures.get(i); System.out.println("第" + (i+1) + "个任务被取消了吗?答案是:" + f.isCancelled()); if (! f.isCancelled()) { System.out.println(f.get()); } } executorService.shutdown(); } } class CallableTest2 implements Callable<Integer> { private int n; public CallableTest2 (int n) { this.n = n; } @Override public Integer call() throws InterruptedException { Thread.sleep(1500); return n; } }
2.3.2、结果
第1个任务被取消了吗?答案是:false 1 第2个任务被取消了吗?答案是:false 2 第3个任务被取消了吗?答案是:true 第4个任务被取消了吗?答案是:true
2.3.3、分析
结果显而易见,和【1.3.3、分析】几乎一模一样的,只是多了超时时间,执行完前两个任务已经花费了1.5s,总可用时间还有0.5s,但是第三个任务也需要1.5s才能执行完,所以执行到一半超时了,所以进入finally超时cancel取消啦。
3、invokeAny3.1、源码
入口函数是invokeAny,一个带超时时间,一个不带。但是两者都是调用的doInvokeAny。源码如下:
// 隶属于下面这个类 // java.util.concurrent.AbstractExecutorService#doInvokeAny /** * tasks:需要批量执行的任务集合。 * timeout:超时时间 * unit:超时时间单位 * * 用于批量执行任务,并获得一个已经成功执行的任务的结果,可设置这批任务总的执行超时时间。 */ private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks, boolean timed, long nanos) throws InterruptedException, ExecutionException, TimeoutException { // 1. 参数校验,安全校验 if (tasks == null) throw new NullPointerException(); int ntasks = tasks.size(); if (ntasks == 0) throw new IllegalArgumentException(); // 2. 将Callable转换成Future的结果集 ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks); // 3. 采取了ecs,ecs就是对对线程池的一个代理包装,采取LinkedBlockingQueue阻塞队列的形式取第一个执行完的结果 ExecutorCompletionService<T> ecs = new ExecutorCompletionService<T>(this); try { ExecutionException ee = null; final long deadline = timed ? System.nanoTime() + nanos : 0L; // 4. 迭代任务 Iterator<? extends Callable<T>> it = tasks.iterator(); // 5. submit执行第一个任务,且将返回结果放到futures里,这是异步执行的。 futures.add(ecs.submit(it.next())); // 任务数量 --ntasks; // 激活任务数量,也就是被submit的任务数量 int active = 1; for (;;) { // 6. 获取返回结果,其实就是LinkedBlockingQueue.poll(); Future<T> f = ecs.poll(); // 6.1 如果没获取到返回结果,也就是代表任务没执行完呢,则继续提交第二个任务 if (f == null) { // 6.2 如果还有第二个任务的话,就继续提交 if (ntasks > 0) { // 任务数量-1 --ntasks; // 提交任务 futures.add(ecs.submit(it.next())); // 任务激活数量+1 ++active; } else if (active == 0) break; // 若配置了超时时间的话则阻塞等待超时时间 else if (timed) { f = ecs.poll(nanos, TimeUnit.NANOSECONDS); if (f == null) throw new TimeoutException(); nanos = deadline - System.nanoTime(); } else // 6.3 如果任务都提交完了,但是还没有一个执行完成的,则在这阻塞等待执行完成拿到结果 f = ecs.take(); } // 7. 若有任务执行完了,能正常返回结果了,则直接return返回 if (f != null) { --active; try { return f.get(); } catch (ExecutionException eex) { ee = eex; } catch (RuntimeException rex) { ee = new ExecutionException(rex); } } } if (ee == null) ee = new ExecutionException(); throw ee; } finally { // 8. 取消其他任务,这里的问题和invokeAll一样的,就是任务都能得到执行,只是最终会返回一个结果,如果任务已经执行了,是无法取消的,如果没有被submit的话是会被interrupt的 for (int i = 0, size = futures.size(); i < size; i++) futures.get(i).cancel(true); } }
3.2、要点总结
3.2.1、核心流程
- 方法含义:用于批量执行任务,并获得第一个已经成功执行的任务的结果
- 具体的线程池采取的是ExecutorCompletionService,ExecutorCompletionService是对线程池的一个代理,一个包装,采取LinkedBlockingQueue阻塞队列的方案来获取执行结果。
- 会逐个submit提交执行任务,只要其中有一个执行完成了,就不会继续提交任务,直接返回当前执行完成的任务的结果,然后finally取消那些未被执行的任务
- cancel是真的取消吗?no!前面执行完成的无法收回,只是取消当前出错的以及后面未执行完成的任务,取消的策略就是interrupt,中断
3.2.2、答疑环节
-
ExecutorCompletionService是干嘛的?原理怎样?
其实就是对Executor线程池的一个包装,然后阻塞队列的形式来存储任务且调用阻塞队列的poll和take方法来等待结果的返回。
public class ExecutorCompletionService<V> implements CompletionService<V> { private final Executor executor; private final AbstractExecutorService aes; private final BlockingQueue<Future<V>> completionQueue; // .... // 采取的就是LinkedBlockingQueue的api,比如poll和take this.completionQueue = new LinkedBlockingQueue<Future<V>>(); public Future<V> take() throws InterruptedException { return completionQueue.take(); } public Future<V> poll() { return completionQueue.poll(); } public Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException { return completionQueue.poll(timeout, unit); } }
-
ExecutorCompletionService采取了什么设计模式?
装饰者设计模式。代码如下:
// java.util.concurrent.ExecutorCompletionService#submit(java.util.concurrent.Callable<V>) public Future<V> submit(Callable<V> task) { if (task == null) throw new NullPointerException(); RunnableFuture<V> f = newTaskFor(task); /* * 装饰者在这里了~~~!!! * QueueingFuture是FutureTask的子类,FutureTask是RunnableFuture的子类,相当于QueueingFuture和FutureTask都有相同的父类,然后通过构造器将FutureTask包装成QueueingFuture,装饰者模式想想IO流 */ executor.execute(new QueueingFuture(f)); return f; } private class QueueingFuture extends FutureTask<Void> { QueueingFuture(RunnableFuture<V> task) { super(task, null); this.task = task; } }
3.3、Demo
两个线程执行两个任务,每个任务sleep不同时长,然后看看发生什么情况?
3.3.1、代码
public class AbstractExecutorServiceTest { public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(2); List<CallableTest2> callableList = new ArrayList(); callableList.add(new CallableTest2(1)); callableList.add(new CallableTest2(2)); Integer integer = executorService.invokeAny(callableList); System.out.println(integer); executorService.shutdown(); } } class CallableTest2 implements Callable<Integer> { private int n; public CallableTest2 (int n) { this.n = n; } @Override public Integer call() { System.out.println(n + "被执行了"); try { Thread.sleep(n * 100); } catch (InterruptedException e) { e.printStackTrace(); } return n; } }
3.3.2、结果
1被执行了 2被执行了 1 java.lang.InterruptedException: sleep interrupted
3.3.3、分析
-
多个任务都得到执行,但是只取第一个执行完的结果
-
那么第二个为啥抛出了InterruptedException呢?
是因为我们第一个任务执行完了,第二个任务还在sleep中,因为有任务执行完了,所以退出循环,走到finally去cancel其他任务,cancel的原理就是interrupt,所以interrupt刚才的sleep线程,所以抛出了InterruptedException。
-
模板方法模式,很经典。可以参考学习应用到业务系统中。
-
编码技巧:用状态量表示是否完成,判断未执行完成的话进行阻塞等待,自由选择忽略哪些异常,抛出其他错误的话跳到finally里进行cancel取消任务。
-
程序的严谨性,比如invokeAll带超时时间的,超时时间计算的各种细节,各种情况全考虑在内。
-
良好的命名规范,xxx是入口api,底层实现的方法命名为doXxx。