记一次线程饥饿死锁的BUG

Executors.newFixedThreadPool(threads)通过设定相同的核心线程数和最大线程数以及无界的任务队列来实现固定线程数的线程池。由于可用线程数固定,当没有空闲线程来执行新任务时,会提交到任务队列里面等待线程空闲时执行。
我所遇到的问题场景是执行一个异步任务,通过固定线程数的线程池来提交任务,任务本身需要并发处理多个子任务,也复用这个线程池来处理了,简化版代码如下:

ExecutorService service = Executors.newFixedThreadPool(2);//将固定线程数改小方便测试
    
    public void test() throws Throwable
    {
        Future future = service.submit(()->{
            List> tasks = new ArrayList>();
            for (int i = 0; i < 10; i++)
            {
                tasks.add(()->{return 1;});
            }
            List> futures = service.invokeAll(tasks);
            return futures.stream().mapToInt(f->{
                try {
                    return f.get();
                } catch (Exception e) {
                }
                return 0;
            }).sum();
        });
        
        System.err.println(future.get());
    }

如果一次只有单线程执行test方法,那么执行流程是没有问题。而如果多线程同时执行test方法,那么就有可能发生线程饥饿死锁,原因如下:

  1. 线程1往线程池中提交了任务,假设此时线程池有两个空闲线程,则会有一个线程用来执行submit方法提交任务。
  2. 在上面提交的任务还没执行到invokerAll时,线程2也提交了任务,线程池中另一个线程用来执行线程2submit的任务了。
  3. 此时线程池中两个线程都被占用,所以两次执行到invokeAll的子任务都会被放入线程池的任务队列等待空闲线程来执行。
  4. 但是invokeAll本身是阻塞获取结果的,也就是,只有子任务tasks都执行完毕,submit提交的总任务才能完成。这样一来就形成了子任务tasks在等待线程池中空闲线程来执行,而另一方面,线程池又必需等待tasks执行完成才有空闲线程出来,造成了线程饥饿死锁,任务进行不下去。

问题的根本原因是执行线程数固定,父任务把线程消耗完时,子任务无法执行,而父任务又是等待子任务执行完成才能完成。解决的方法有两个,一是父任务和子任务通过不同的线程池来执行,避免相互影响。二是使用更合适的ForkJoinPool来代替FixedThreadPool来执行这种嵌套任务。

你可能感兴趣的:(记一次线程饥饿死锁的BUG)