文章目录

  • 如果下面的问题你都会的话就别在这浪费时间啦
  • 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、invokeAll

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、要点总结

  1. 方法含义:用于批量执行任务,并且将结果按照task列表中的顺序返回

  2. 将Callable转成FutureTask且放到List中,目的是FutureTask可以取消任务也可以拿到返回结果

  3. 采取模板设计模式来完成任务的具体执行(execute方法)

  4. 循环遍历每一个任务,如果中途某个任务没执行完,则调用get方法阻塞等待,一直等待执行完毕后才会进入下一次for循环来执行其他任务

  5. 忽略CancellationException和ExecutionException两个异常

  6. 若任务执行失败(抛出了忽略的两种异常之外的异常),则进行取消每个任务(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、核心流程

  1. 方法含义:用于批量执行任务,并且将结果按照task列表中的顺序返回,可设置这批任务总的执行超时时间。

  2. 将Callable转成FutureTask且放到List中,目的是FutureTask可以取消任务也可以拿到返回结果

  3. 采取模板设计模式来完成任务的具体执行(execute方法)

  4. 循环遍历每一个任务,如果中途某个任务没执行完,则调用get方法阻塞等待,一直等待执行完毕后才会进入下一次for循环来执行其他任务,若或者等待超时的话会抛出TimeoutException,被catch住,直接返回任务数组

  5. 忽略CancellationException和ExecutionException两个异常,遇到TimeoutException的话会直接return任务数组

  6. 若任务执行失败(抛出了忽略的两种异常之外的异常),则进行取消每个任务(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、invokeAny

3.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、核心流程

  1. 方法含义:用于批量执行任务,并获得第一个已经成功执行的任务的结果
  2. 具体的线程池采取的是ExecutorCompletionService,ExecutorCompletionService是对线程池的一个代理,一个包装,采取LinkedBlockingQueue阻塞队列的方案来获取执行结果。
  3. 会逐个submit提交执行任务,只要其中有一个执行完成了,就不会继续提交任务,直接返回当前执行完成的任务的结果,然后finally取消那些未被执行的任务
  4. 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。

4、从中学到了什么思想?
  • 模板方法模式,很经典。可以参考学习应用到业务系统中。

  • 编码技巧:用状态量表示是否完成,判断未执行完成的话进行阻塞等待,自由选择忽略哪些异常,抛出其他错误的话跳到finally里进行cancel取消任务。

  • 程序的严谨性,比如invokeAll带超时时间的,超时时间计算的各种细节,各种情况全考虑在内。

  • 良好的命名规范,xxx是入口api,底层实现的方法命名为doXxx。