一场由CompletableFuture和ExecutorService引发的血案


现象

程序运行过程中无缘无故卡住,方法执行过程中停滞不前

解决

  1. 根据前段请求找出哪个方法卡住了,发现了方法m

  2. 本地debug发现每次进到m方法里面就卡住

  3. 不给CompletableFuture传入ExecutorService参数(默认是ForkJoinPool)就不会卡死,一度严重怀疑是我们ExecutorService配置出了问题

  4. m方法里面大量使用了CompletableFuture以及注入的线程池

  5. 怀疑是线程耗尽,但是我们的队列配置得比较大,卡住的时候观察了队列,还剩下很多位置,原则上会执行很慢,但是不会卡住

  6. 于是将线程池数量从16改为200,果然成功执行,那么说明确实是线程耗尽

  7. 配置spring线程池:spring.task.execution.pool.allow-core-thread-timeout=true,意思是:core线程在闲时也会销毁

  8. 发现本地即使没有请求,线程也一直不销毁,基本上就是死锁了

  9. 分析业务代码,类似如下(类似下面代码):

        @Test
        void test() throws Exception {
    		// 使用此就会卡住
            ExecutorService pool = Executors.newFixedThreadPool(1);
            // 使用此就不会
            ExecutorService pool = ForkJoinPool.commonPool();
    
            CompletableFuture resp = CompletableFuture.supplyAsync(() -> {
                CompletableFuture result = CompletableFuture.supplyAsync(() -> {
                    try {
                        SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return "dd";
                }, pool);
                try {
                    result.get();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return "ok";
            }, pool);
            resp.get();
        }
    
  10. 外层全部占用线程池的线程,而里面有需要等待内层的CompletableFuture返回结果,而内层又需要等待外层释放线程

结果

  1. 死锁造成的,ExecutorService配置的足够大就不会出错,但是这不治根
  2. 为什么forkjoinpool就不会出错呢?forkjoinpool会源源不断的创建线程

解决方案

不要嵌套使用CompletableFuture

相关