和朱晔一起复习Java并发(一):线程池 (20)

在这里,我们的任务很简单就是不断++一个AtomicLong。测试结果如下:

image_1dflbna1ncr2484ter12m7sir58.png-116.8kB


可以看到,很明显forkjoin更快。

线程池的任务出了异常会怎么样?

经常发现有一些小伙伴写代码,在把任务提交到线程池后,任务经常会出异常,出异常也不管,他们说没事,线程池会捕获异常的。真的是这样吗?写段代码试试:

@Slf4j public class ThreadPoolExceptionTest { @Before public void setDefaultUncaughtExceptionHandler(){ Thread.setDefaultUncaughtExceptionHandler((Thread t, Throwable e)->{ log.warn("Exception in thread {}", t,e); }); } @Test public void test() throws InterruptedException { String prefix = "test"; ExecutorService threadPool = Executors.newFixedThreadPool(1, new ThreadFactoryImpl(prefix)); IntStream.rangeClosed(1, 10).forEach(i -> threadPool.execute(() -> { if (i == 5) throw new RuntimeException("error"); log.info("I'm done : {}", i); if (i < 5) Assert.assertEquals(prefix + "1", Thread.currentThread().getName()); else Assert.assertEquals(prefix + "2", Thread.currentThread().getName()); })); threadPool.shutdown(); threadPool.awaitTermination(1, TimeUnit.HOURS); } }

在这里我们一共有10个任务,执行到第五个任务的时候,会主动抛出一个异常。
从断言可以看到,一开始的任务是在test1线程上执行的,因为出了异常后面的任务就在test2线程上执行了,看看运行结果:

image_1dflca9f9rdi4iiqbaj4m2bk5l.png-389.5kB


结果上也可以看到几个结论:

线程池里的任务出了未处理异常,最终这个异常算是未处理异常,可以由未处理异常进行处理

这个线程会终止,而不是传说的线程池会捕获异常

线程池只能重新创建新的异常来填补空白,这是有代价的

不过这个结论不完全正确,我们尝试把execute改为submit来试试:

@Test public void test() throws InterruptedException { String prefix = "test"; ExecutorService threadPool = Executors.newFixedThreadPool(1, new ThreadFactoryImpl(prefix)); List<Future> futures = new ArrayList<>(); IntStream.rangeClosed(1, 10).forEach(i -> futures.add(threadPool.submit(() -> { if (i == 5) throw new RuntimeException("error"); log.info("I'm done : {}", i); // if (i < 5) Assert.assertEquals(prefix + "1", Thread.currentThread().getName()); // else Assert.assertEquals(prefix + "2", Thread.currentThread().getName()); }))); for (Future future : futures) { try { future.get(); } catch (ExecutionException e) { log.warn("future ExecutionException",e); } } threadPool.shutdown(); threadPool.awaitTermination(1, TimeUnit.HOURS); }

输出结果如下:

image_1dfld304t1jp4c1og8d13mp3sh62.png-484kB


可以看到这次不一样了,异常会在Future.get()的时候拿到,因此线程也不会死亡。
我们再写一段代码比较下性能(需要注释setDefaultUncaughtExceptionHandler中的log.warn避免输出异常):

@Test public void test2() throws InterruptedException { StopWatch stopWatch = new StopWatch(); ExecutorService threadPool1 = Executors.newFixedThreadPool(1); stopWatch.start("execute"); IntStream.rangeClosed(1, 100000).forEach(i->threadPool1.execute(()->{ throw new RuntimeException("error"); })); threadPool1.shutdown(); threadPool1.awaitTermination(1, TimeUnit.HOURS); stopWatch.stop(); ExecutorService threadPool2 = Executors.newFixedThreadPool(1); stopWatch.start("submit"); IntStream.rangeClosed(1, 100000).forEach(i->threadPool2.submit(()->{ throw new RuntimeException("error"); })); threadPool2.shutdown(); threadPool2.awaitTermination(1, TimeUnit.HOURS); stopWatch.stop(); log.info(stopWatch.prettyPrint()); }

结果如下:

image_1dfldk31i7h616dgu2odk6clg6f.png-78.1kB


这就更证实了之前说的重新创建线程的代价问题。你还可以自己测试一下,定时任务线程池的任务出异常了会怎么样?

代码

本文的所有代码见我的Github Repo:https://github.com/JosephZhu1983/java-concurrent-test
代码的测试基于JDK11

总结

本文我们做了十来个实验,各种测试线程池。总结下来几点:

线程池***手动创建,不要用系统所谓的预定义的常用线程池

在极端情况下可能需要修改默认线程池创建线程的行为

注意线程池中的任务建议还是不要出现未处理异常为好

使用定时任务线程池进行任务执行注意线程池大小问题

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgdxwx.html