JAVA多线程问题 — 如何正确使用异步线程和线程执行器?

先看下线程池最基本的用法示例;

1、ThreadPoolExecutor

使用ThreadPoolExecutor 定义一个线程池:

/**
* 线程池
*/
private static final ExecutorService executorService = new ThreadPoolExecutor(3, 10, 60, TimeUnit.SECONDS,
       new ArrayBlockingQueue<>(100),
       r -> {
           Thread thread = new Thread(r);
           thread.setName("myThreadPoolExecutor");
           //设置异常捕获器
           thread.setUncaughtExceptionHandler((t, e) -> log.error("[message]async exec task error! e:{}", e.getMessage()));
           return thread;
       }, new ThreadPoolExecutor.AbortPolicy());

接下来分别依次提交3个任务A/B/C,任务类型为有返回值的Callable task,每个任务的执行时间不同;异步任务提交的结果为Future类型,提交后接下来按序对每个Future对象调用future.get方法获取其结果,代码如下:

/**
 * 测试ExecutorService获取异步结果的顺序及实际执行时间
 */
private static void testThreadPool() {
    List<Future<String>> futureList = Lists.newArrayList();
    // 记录A/B/C的任务完成时间
    List<AtomicLong> taskFinshTimeList = Lists.newArrayList();
    AtomicLong finishA = new AtomicLong();
    AtomicLong finishB = new AtomicLong();
    AtomicLong finishC = new AtomicLong();
    // A cost 10s
    final Future<String> futureA = executorService.submit(() -> {
        log.warn("exec A start");
        final long start = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            log.error("A InterruptedException occur!");
        }
        finishA.set(System.currentTimeMillis());
        taskFinshTimeList.add(finishA);
        log.warn("exec A finish cost=[{}]ms", finishA.get() - start);
        return "A";
    });
    futureList.add(futureA);
 
    // B cost 3s
    final Future<String> futureB = executorService.submit(() -> {
        log.warn("exec B start");
        final long start = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            log.error("B InterruptedException occur!");
        }
        finishB.set(System.currentTimeMillis());
        taskFinshTimeList.add(finishB);
        log.warn("exec B finish cost=[{}]ms", finishB.get() - start);
        return "B";
    });
    futureList.add(futureB);
 
    // C cost 7s
    final Future<String> futureC = executorService.submit(() -> {
        log.warn("exec C start");
        final long start = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(7);
        } catch (InterruptedException e) {
            log.error("C InterruptedException occur!");
        }
        finishC.set(System.currentTimeMillis());
        taskFinshTimeList.add(finishC);
        log.warn("exec C finish cost=[{}]ms", finishC.get() - start);
        return "C";
    });
    futureList.add(futureC);
 
    // 同步获取结果
    AtomicInteger taskIndex = new AtomicInteger();
    futureList.forEach(future -> {
        try {
            final String result = future.get();
            // 哪怕使用Future#get(long, java.util.concurrent.TimeUnit)方法,也不能使得当前异步任务执行完后立即就能拿出结果
            // final String result = future.get(5, TimeUnit.SECONDS);
            log.warn("sync get result, then do next task using [result={}], waiting [{}]ms after task finish.",
                    result, System.currentTimeMillis() - taskFinshTimeList.get(taskIndex.getAndIncrement()).get());
        } catch (Exception e) {
            log.error("future#get error occur!");
        }
    });
 
    executorService.shutdown();
}

三个任务,每个任务执行时间分别是 10s、3s、7s 。通过 JDK 线程池的 submit 提交这三个 Callable 类型的任务。

  • 第一步:主线程把三个任务提交到线程池里面去,把对应返回的 Future 放到 List 里面存起来。
  • 第二步:在循环里面执行 future.get() 操作,阻塞等待。

代码中,我打印了每个任务的执行开始时间、任务执行结束时间、从Future对象获取到异步结果的时间,以及从任务执行结束到获取异步结果这之间等待的时间;

根据我们的尝试,Future.get方法会阻塞主线程,因此我们预期的结果就是:尽管异步任务的执行结束的顺序依次为:B/C/A,但是由于是按照A/B/C的顺序从Future对象获取结果,因此实际获取到异步结果的顺序依次为A/B/C,下面是执行结果:

 [myThreadPoolExecutor] exec B start
 [myThreadPoolExecutor] exec A start
 [myThreadPoolExecutor] exec C start
 [myThreadPoolExecutor] exec B finish cost=[3001]ms
 [myThreadPoolExecutor] exec C finish cost=[7001]ms
 [myThreadPoolExecutor] exec A finish cost=[10001]ms
 [main] WARN sync get result, then do next task using [result=A], waiting [7000]ms after task finish.
 [main] WARN sync get result, then do next task using [result=B], waiting [3000]ms after task finish.
 [main] WARN sync get result, then do next task using [result=C], waiting [0]ms after task finish.

可以看到与我们的预期一致:先完成的2个任务B和C由于比A后调用Future#get(),尽管任务已经执行完了,但也要等到执行时间最长的任务A执行完并且Future#get()拿到结果后,才能通过Future#get()拿到B和C各自的结果,中间分别等了7s和3s;

如果都按照ExecutorService的线程池同步获取异步任务结果的这种方式,并且恰巧前几个异步任务调用的接口耗时比较久,那么获取异步结果的时候就比较悲催了,因为后面的执行更快的异步结果获取会阻塞等待;

那么就需要用到以下这个线程执行器,get的时候能根据异步任务完成的顺序get出来,让获取异步结果这一行为不阻塞,叫ExecutorCompletionService。

2、 ExecutorCompletionService

使用时直接将线程池作为其构造函数的入参即可,因为API与ExecutorService基本一致,因此代码基本不需要改动;

/**
 * ExecutorCompletionService
 */
private static final CompletionService<String> completionService = new ExecutorCompletionService(executorService);

测试代码如下:

@SneakyThrows
private static void testCompletionService() {
    List<Future<String>> futureList = Lists.newArrayList();
    // 记录A/B/C的任务完成时间
    List<AtomicLong> taskFinshTimeList = Lists.newArrayList();
    AtomicLong finishA = new AtomicLong();
    AtomicLong finishB = new AtomicLong();
    AtomicLong finishC = new AtomicLong();
    // A cost 10s
    final Future<String> futureA = completionService.submit(() -> {
        log.warn("exec A start");
        final long start = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            log.error("A InterruptedException occur!");
        }
        finishA.set(System.currentTimeMillis());
        taskFinshTimeList.add(finishA);
        log.warn("exec A finish cost=[{}]ms", finishA.get() - start);
        return "A";
    });
    futureList.add(futureA);

    // B cost 3s
    final Future<String> futureB = completionService.submit(() -> {
        log.warn("exec B start");
        final long start = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            log.error("B InterruptedException occur!");
        }
        finishB.set(System.currentTimeMillis());
        taskFinshTimeList.add(finishB);
        log.warn("exec B finish cost=[{}]ms", finishB.get() - start);
        return "B";
    });
    futureList.add(futureB);

    // C cost 7s
    final Future<String> futureC = completionService.submit(() -> {
        log.warn("exec C start");
        final long start = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(7);
        } catch (InterruptedException e) {
            log.error("C InterruptedException occur!");
        }
        finishC.set(System.currentTimeMillis());
        taskFinshTimeList.add(finishC);
        log.warn("exec C finish cost=[{}]ms", finishC.get() - start);
        return "C";
    });
    futureList.add(futureC);

//        // 同步获取结果依旧会阻塞
//        AtomicInteger taskIndex = new AtomicInteger();
//        futureList.forEach(future -> {
//            try {
//                final String result = future.get();
//                log.warn("sync get result, then do next task using [result={}], waiting [{}]ms after task finish.",
//                        result, System.currentTimeMillis() - taskFinshTimeList.get(taskIndex.getAndIncrement()).get());
//            } catch (Exception e) {
//                log.error("future#get error occur!");
//            }
//        });

// 调用completionService.take方法获取异步结果
    for (int i = 0; i < futureList.size(); i++) {
        final String result = completionService.take().get();
        log.warn("completionService.take() [result={}]", result);
    }

    executorService.shutdown();
}

与上面的例子一样,3个任务,任务的执行时间这些条件都一样,区别在于通过CompletionService.take方法获取异步结果,主要关注下同步获取异步任务结果的测试结果;

 [myThreadPoolExecutor] exec C start
 [myThreadPoolExecutor] exec B start
 [myThreadPoolExecutor] exec A start
 [myThreadPoolExecutor] exec B finish cost=[3001]ms
 [main] completionService.take() [result=B]
 [myThreadPoolExecutor] exec C finish cost=[7001]ms
 [main] completionService.take() [result=C]
 [myThreadPoolExecutor] exec A finish cost=[10001]ms
 [main] completionService.take() [result=A]

从结果可知,每个异步任务执行完成后马上就能拿到异步结果,不会发生阻塞,这样的好处就是,前序异步任务执行完成后,马上就能拿到结果,紧接着只能执行后续的流程处理;

但是如果我们直接拿CompletionService做ExecutorService的替换,并且恰恰我们不需要用到异步线程的执行结果(如Runnable类型的异步任务)时,就会出问题,可能引发系统OOM!

接下来看下CompletionService的源码,他怎么能做到通过CompletionService.take方法,就能按照异步任务的执行完成顺序获取异步结果呢?

3、CompletionService的源码分析

(1)CompletionService接口

public interface CompletionService<V> {
    /**
     * 提交一个返回值的任务执行,并返回一个Future对象
     * 代表任务的待定结果。任务完成后,
     * 这个任务可以被取出或轮询。
     *
     * @param task 要提交的任务
     * @return 一个Future对象,代表任务的待定完成
     * @throws RejectedExecutionException 如果任务不能被
     *         安排执行
     * @throws NullPointerException 如果任务是空的
     */
    Future<V> submit(Callable<V> task);
    /**
     * 提交一个Runnable任务执行,并返回一个Future对象
     * 代表这个任务。任务完成后,
     * 这个任务可以被取出或轮询。
     *
     * @param task 要提交的任务
     * @param result 成功完成后返回的结果
     * @return 一个Future对象,代表任务的待定完成,
     *         并且其{@code get()}方法将在完成时返回给定的
     *         结果值
     * @throws RejectedExecutionException 如果任务不能被
     *         安排执行
     * @throws NullPointerException 如果任务是空的
     */
    Future<V> submit(Runnable task, V result);
    /**
     * 检索并移除代表下一个
     * 完成的任务的Future对象,如果没有则等待。
     *
     * @return 一个Future对象,代表下一个完成的任务
     * @throws InterruptedException 如果在等待时被中断
     */
    Future<V> take() throws InterruptedException;
    /**
     * 检索并移除代表下一个
     * 完成的任务的Future对象,如果没有则返回{@code null}。
     *
     * @return 一个Future对象,代表下一个完成的任务,或者
     *         {@code null}如果没有
     */
    Future<V> poll();
}

(2)ExecutorCompletionService是CompletionService接口唯一的的实现类;

public class ExecutorCompletionService<V> implements CompletionService<V> {
    private final Executor executor;
    private final AbstractExecutorService aes;
    private final BlockingQueue<Future<V>> completionQueue;
	...
	...
}

这里关注2个属性:线程池executor、阻塞队列completionQueue;

(3)对比ExecutorService和ExecutorCompletionService的submit方法可以看出区别

这个是ExecutorService的submit方法:

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

这个是ExecutorCompletionService的submit方法:

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;
}

主要区别就在于上图标记的QueueingFuture,继续跟进去看:

 /**
  * FutureTask extension to enqueue upon completion
  */
 private class QueueingFuture extends FutureTask<Void> {
     QueueingFuture(RunnableFuture<V> task) {
         super(task, null);
         this.task = task;
     }
     protected void done() { completionQueue.add(task); }
     private final Future<V> task;
 }

QueueingFuture继承自FutureTask,它重写了done()方法;重写后的逻辑为:当任务执行完成后,task就会被放到completionQueue队列里;也就是说,completionQueue队列里面的task都是已经done()完成了的task,这个task就是我们拿到的一个个的future结果;队列取出元素的顺序就是任务的完成顺序;

如果调用队列completionQueue的task方法,会阻塞等待任务完成,直到某个任务完成后被插入队列;一旦从队列取出一个元素,这个元素一定是完成了的future,我们调用Future#get方法当然就能直接获得结果;

总结下来,对比ExecutorService和ExecutorCompletionService二者,可知:我们在使用 ExecutorService#submit提交任务后需要关注每个任务返回的future何时才能完成;然而 CompletionService重写了done方法,对这些future进行了追踪,保证completionQueue队列里面一定是完成了的task,可以立即从future中get任务执行结果;

上面说了"CompletionService执行无返回值的Runnable类型的异步任务时,可能引发系统OOM!" 为什么呢?

原因很简单,因为我们使用了阻塞队列,这个队列会跟随完成的异步任务而加入,当这个队列没有将元素poll出来时,队列就会不断增长,占用内从就会越来越大,最终可能引发OOM;

什么情况会将completionQueue队列的元素取出呢?以下三个方法:

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);
}

注意,直接调用Future#get并不能从completionQueue队列移除元素;对于有返回值的Callable对象,一般我们关心其返回值(执行结果),因此会显示的把task给task/poll出来;但是,对于无返回值的Runnable任务,我们很难会主动的在代码中尝试去take/poll一下,因此更容易导致队列一直未移除元素,最终内存不断增长;因此,不能直接拿ExecutorCompletionService作为ExecutorService的替换。

4、推荐使用CompletableFuture

上面我们使用ExecutorCompletionService是因为我们并行的执行了多个异步任务,并且希望各个任务执行完后,能立即拿着结果去做下一件事;这里我推荐使用JDK8引入的组合式异步编程,下面是代码示例,它可以通过ofAll将多个异步任务一起同步尝试获取执行结果、可以使用JDK自带的forkJoinPool也可以自己制定线程池,并且可以通过提供的thenApply/thenRun/thenAccept等API灵活的实现流式异步编程,结合lambda表达式,代码清晰简洁明了;

/**
 * 测试CompletableFuture获取异步结果的顺序及异步任务执行时间
 */
@SneakyThrows
private static void testCompletableFuture() {
    List<CompletableFuture<String>> futureList = Lists.newArrayList();
    // 记录A/B/C的任务完成时间
    List<AtomicLong> taskFinshTimeList = Lists.newArrayList();
    AtomicLong finishA = new AtomicLong();
    AtomicLong finishB = new AtomicLong();
    AtomicLong finishC = new AtomicLong();
    // A cost 10s
    final CompletableFuture<String> completableFutureA = CompletableFuture.supplyAsync(() -> {
        log.warn("exec A start");
        final long start = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            log.error("A InterruptedException occur!");
        }
        finishA.set(System.currentTimeMillis());
        taskFinshTimeList.add(finishA);
        log.warn("exec A finish cost=[{}]ms", finishA.get() - start);
        return "A";
    }, executorService);

    futureList.add(completableFutureA);

    // B cost 3s
    final CompletableFuture<String> completableFutureB = CompletableFuture.supplyAsync(() -> {
        log.warn("exec B start");
        final long start = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            log.error("B InterruptedException occur!");
        }
        finishB.set(System.currentTimeMillis());
        taskFinshTimeList.add(finishB);
        log.warn("exec B finish cost=[{}]ms", finishB.get() - start);
        return "B";
    }, executorService);
    futureList.add(completableFutureB);

    // C cost 7s
    final CompletableFuture<String> completableFutureC = CompletableFuture.supplyAsync(() -> {
        log.warn("exec C start");
        final long start = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(7);
        } catch (InterruptedException e) {
            log.error("C InterruptedException occur!");
        }
        finishC.set(System.currentTimeMillis());
        taskFinshTimeList.add(finishC);
        log.warn("exec C finish cost=[{}]ms", finishC.get() - start);
        return "C";
    }, executorService);
    futureList.add(completableFutureC);

    // 同步获取结果
//        AtomicInteger taskIndex = new AtomicInteger();
//        futureList.forEach(future -> {
//            try {
//                // 执行完取异步任务的结果不用阻塞 执行完立即去做下一件事 并且可以组合多个异步任务
//                future
//                        // 第二件事 有返回值
//                        .thenApply(result -> {
//                            log.warn("sync get result, then do next task using [result={}], waiting [{}]ms after step.1 task finish.",
//                                    result, System.currentTimeMillis() - taskFinshTimeList.get(taskIndex.getAndIncrement()).get());
//                            return "secondary_" + result;
//                        })
//                        // 第三件事 无返回值
//                        .thenAccept(secondaryResult -> {
//                            log.warn("sync get secondary result, then do next task using [secondary result={}]", secondaryResult);
//                        });
//            } catch (Exception e) {
//                log.error("future#get error occur!");
//            }
//        });

    AtomicInteger index = new AtomicInteger();
    CompletableFuture[] array = new CompletableFuture[futureList.size()];
    futureList.forEach(completableFuture -> array[index.getAndIncrement()] = completableFuture);
    // 获取最早得到结果的那个异步任务的返回值 其他的异步任务还在执行不会中断
    final Object anyResult = CompletableFuture.anyOf(array).get();
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RachelHwang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值