首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >线程池submit和execute,搞不好会引发线上故障

线程池submit和execute,搞不好会引发线上故障

作者头像
用户7634691
发布2020-08-10 16:02:02
1.9K0
发布2020-08-10 16:02:02
举报

写在前面

这两个方法都可以用来提交任务给线程池,但是又有所区别。我们先来看下二者的使用示例,先有个直观认识。

execute的使用示例:

public class ExecuteTest {
    private final static ThreadPoolExecutor executor = new ThreadPoolExecutor(0,1,0, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        while (true) {
            executor.execute(() -> {
                System.out.println(atomicInteger.getAndAdd(1));
            });
        }
    }
}

submit使用示例:

public class SubmitTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable = new Callable<String>() {
            public String call() throws Exception {
                System.out.println("This is submit method.");
                return "submit is done";
            }
        };

        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(callable);
        System.out.println(future.get());
    }
}

从上面的示例可以看出二者的区别:

execute的入参是Runnable, 没有返回值。任务通过execute提交后就基本和主线程脱离关系了。

submit的入参可以是Callable(也可以是Runnable),并且有返回值,返回的是一个Future对象,然后通过对象的get方法获取任务执行的结果。

平时使用的时候根据不同的业务场景决定使用哪个方法。如果使用不当的话可能会引起很严重的问题,下面就带你看几个例子。

任务统计导致OOM问题

下面这个例子是统计任务执行的次数,

/**
     * 统计任务已经执行的数量
     * key:任务名称
     * value:数量
     */
    private static Map<String, AtomicInteger> countTasks = new ConcurrentHashMap<>();

    public static void  main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()){
            /**
             * 任务执行完后统计任务执行的数量
             * @param r
             * @param t
             */
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                super.afterExecute(r, t);
                countTasks.compute(r.toString(), (s, atomicInteger) ->
                        new AtomicInteger(atomicInteger == null ? 0 : atomicInteger.incrementAndGet()));
            }
        };
        /**
         * 源源不断的任务添加进线程池被执行
         */
        for (int i =0; i < 100; i++) {
            threadPoolExecutor.execute(new SimpleRunnable());
        }
        CountDownLatch cd = new CountDownLatch(1);
        cd.await(10, TimeUnit.SECONDS);
        System.out.println(JSON.toJSONString(countTasks));
        threadPoolExecutor.shutdownNow();
    }
    static class SimpleRunnable implements Runnable{

        @Override
        public void run() {
            System.out.println("simple task");
        }

        @Override
        public String toString(){
            return this.getClass().getSimpleName();
        }
    }

执行的结果是:

{"SimpleRunnable":99}

这个是正常的,我们有一个任务,提交了99次到线程池,任务被执行了99次。

如果把execute改成submit,结果是怎样呢?如下:

{"java.util.concurrent.FutureTask@76f2781a":0,"java.util.concurrent.FutureTask@74bbc5ae":0,"java.util.concurrent.FutureTask@e34a8ea":0,"java.util.concurrent.FutureTask@2fd1ff2":0,"java.util.concurrent.FutureTask@2ec135dd":0,"java.util.concurrent.FutureTask@51e8edb8":0 
....

我省略了打印结果其余的部分,不过你应该已经看出来了,改成submit后,每次提交任务都是一个新的任务名,也就是SimpleRunnabletoString方法每次返回的值都不同。这个在线上的话我们的map很容易就被撑爆了。

为什么会出现这种情况呢?我们看下submit里面都干了啥,

如上图所示,线程池通过submit方式提交任务,会把Runnable封装成FutrueTask,每次提交都会创建一个新的实例。我们debug一下,把断点设置在afterExecute方法里,如下图,r参数确实是FutrueTask实例。

接着看,还有坑

异常无法抛出的问题

public static void  main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

        List<Integer> list = Lists.newArrayList(1, 2, 3, null);

        threadPoolExecutor.submit(() -> {
            List<String> result = list.stream().map(a -> a.toString()).collect(Collectors.toList());
            System.out.println(JSON.toJSONString(result));
        });

        CountDownLatch cd = new CountDownLatch(1);
        cd.await(2, TimeUnit.SECONDS);
        threadPoolExecutor.shutdownNow();
    }

list里面包含一个null元素,很显然在执行list.stream().map时会报错,但是你运行下看看会发现什么也没发生。(不着急,等下再说原因),然后改成execute试试,运行会报如下的错误:

Exception in thread "pool-1-thread-1" java.lang.NullPointerException
    at com.app.GCTest.lambda$null$0(GCTest.java:28)
    at com.app.GCTest$$Lambda$2/215004560.apply(Unknown Source)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
    at com.app.GCTest.lambda$main$1(GCTest.java:28)
    at com.app.GCTest$$Lambda$1/636718812.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:744)

嗯,这个跟我们预期的一样。

这可是个大坑啊,必须要深挖一下原因。顺着submit一层层进入源码中,最终Runnable接口的run方法,前面说了submit提交的任务会被包装成FutureTask实例,所有最终是调用它的run方法,源码如下:

很明显,异常在内部被“吃掉了”。不过从源码可以看到异常被set出来了,应该是可以用get主动拉取到的。我们可以来做个测试。

在上面的代码示例中加入如下:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()){
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                super.afterExecute(r, t);
                if (t != null) {
                    t.printStackTrace();
                } else {
                    if (r instanceof Future<?>) {
                        try {
                            //get这里会首先检查任务的状态,然后将上面的异常包装成ExecutionException
                            Object result = ((Future<?>) r).get();
                        } catch (CancellationException ce) {
                            t = ce;
                        } catch (ExecutionException ee) {
                            t = ee.getCause();
                            t.printStackTrace();
                        } catch (InterruptedException ie) {
                            Thread.currentThread().interrupt(); // ignore/reset
                        }
                    }
                }
            }
        };

然后再运行,发现异常信息可以正常打印了。

所以,如果使用submit,要么用get拉取异常处理, 要么自己写try catch把任务执行的逻辑包起来。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 犀牛的技术笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 任务统计导致OOM问题
  • 异常无法抛出的问题
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档