编码踩坑——多线程可能带来意想不到的OOM

16 篇文章 4 订阅
5 篇文章 0 订阅

多线程技术数据Java基础中比较重要的知识点;实际使用时,我们往往会用线程池来管理我们的异步线程,本篇介绍一个由于使用ExecutorCompletionService多线程可能导致的OOM问题,开发时要千万小心,即使用一套JDK API时一定要熟悉其原理和可能踩的坑,不要只看到其好处拿来就用,搞不好就要出线上事故;

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

1. 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<String> task,每个任务的执行时间不同,分别为10s/3s/7s;异步任务提交的结果为Future<String>类型,提交后接下来按序对每个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();
    }

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

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

17:53:25.733 [myThreadPoolExecutor] exec B start
17:53:25.733 [myThreadPoolExecutor] exec A start
17:53:25.733 [myThreadPoolExecutor] exec C start
17:53:28.737 [myThreadPoolExecutor] exec B finish cost=[3001]ms
17:53:32.737 [myThreadPoolExecutor] exec C finish cost=[7001]ms
17:53:35.737 [myThreadPoolExecutor] exec A finish cost=[10001]ms
17:53:35.737 [main] WARN sync get result, then do next task using [result=A], waiting [7000]ms after task finish.
17:53:35.737 [main] WARN sync get result, then do next task using [result=B], waiting [3000]ms after task finish.
17:53:35.737 [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;

看到这里,尤其是做C端业务的同学可能会有点想法,一般来说,C端业务在处理一次请求时会调用下游N个接口(很多下游,如今日头条,一次用户刷新请求可能调用用户画像、视频推荐、帖子推荐、游戏推荐、广告推荐等多个业务方接口,对结果做聚合);

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

这个时候,你可能百度了一下,能否有一种线程执行器,get的时候能根据异步任务完成的顺序get出来,让获取异步结果这一行为不阻塞呢?万能的Google告诉你,还只有这么一个东西,叫ExecutorCompletionService,号称"任务规划大师"(时间管理大师?[doge]),下面来试试;

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方法获取异步结果,主要关注下同步获取异步任务结果的测试结果;

20:37:23.857 [myThreadPoolExecutor] exec C start
20:37:23.857 [myThreadPoolExecutor] exec B start
20:37:23.857 [myThreadPoolExecutor] exec A start
20:37:26.860 [myThreadPoolExecutor] exec B finish cost=[3001]ms
20:37:26.861 [main] completionService.take() [result=B]
20:37:30.860 [myThreadPoolExecutor] exec C finish cost=[7001]ms
20:37:30.860 [main] completionService.take() [result=C]
20:37:33.860 [myThreadPoolExecutor] exec A finish cost=[10001]ms
20:37:33.860 [main] completionService.take() [result=A]

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

但是——

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

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

CompletionService的源码分析

(1)CompletionService接口

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

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

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

这个是ExecutorService的submit方法:

这个是ExecutorCompletionService的submit方法:

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

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队列的元素取出呢?以下三个方法:

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

因此,不能直接拿ExecutorCompletionService作为ExecutorService的替换!!!

3.  推荐使用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();
        log.warn("anyResult={}", anyResult);

更多的示例可以参考之前写的这篇文章《Java8 实战》笔记——4.CompletableFuture-组合式异步编程

参考:

多线程使用不当导致的 OOM

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值