性能文章>JDK 线程池使用不当引发的线程饥饿死锁问题>

JDK 线程池使用不当引发的线程饥饿死锁问题原创

352935

前言

使用线程池时会忽略死锁问题, 但是只要代码写的"六"没啥是不可能的

文章代码及部分理念引自 线程池使用不当也会死锁[1]

什么是死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去

此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

死锁定义来源: 死锁-百度百科[2]

简化来说就是: 一组相互竞争资源的线程因为互相等待,导致"永久"阻塞的现象

我们通过一个 Demo 简单理解下

public class DeadlockTest {

    public static void main(String[] args) throws InterruptedException {
        Object o1 = new Object();
        Object o2 = new Object();
        new Thread(() -> {
            synchronized (o1) {
                try {
                    System.out.println(String.format("  >>> %s 获取 o1 锁 ", Thread.currentThread().getName()));
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println(String.format("  >>> %s 获取 o2 锁 ", Thread.currentThread().getName()));
                }
            }
        }, "线程一").start();

        Thread.sleep(10);

        new Thread(() -> {
            synchronized (o2) {
                try {
                    System.out.println(String.format("  >>> %s 获取 o2 锁 ", Thread.currentThread().getName()));
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println(String.format("  >>> %s 获取 o1 锁 ", Thread.currentThread().getName()));
                }
            }
        }, "线程二").start();
    }
    /**
     *   >>> 线程一 获取 o1 锁 
     *   >>> 线程二 获取 o2 锁 
     */

}

在程序中, 线程一首先获取 o1 对象锁, 其后睡眠 50ms, 醒后获取 o2 对象锁

线程二首先获取 o2 对象锁, 其后睡眠 50ms, 醒后获取 o1 对象锁

这样就很微妙的达到了死锁的产生, 这里不再讲述死锁相关的细节

线程池死锁产生的条件

通过代码来模拟线程池产生死锁的前因后果, 有兴趣的小伙伴可以把代码拉到本地跑一下

这段代码表现的场景如下:

1、创建一个 JDK 线程池, 并添加一个线程三秒后打印线程池的信息

2、循环调取 innerFutureAndOutFuture 方法

3、innerFutureAndOutFuture 运行过程为提交十个 Callable 运行

4、但是在提交 Callable 内部另外依赖了 Callable 任务, 并需要获取执行结果

理想情况是 while 循环会不断执行, 并自增 loop

建议电脑观看代码, 或粘贴到编辑器中运行

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class HungryDeadLockTest {

    private static ThreadPoolExecutor executor;

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        TimeUnit unit = TimeUnit.HOURS;
        BlockingQueue workQueue = new LinkedBlockingQueue();
        executor = new ThreadPoolExecutor(551000, unit, workQueue);

        new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(executor);
        }).start();

        int loop = 0;
        while (true) {
            System.out.println("loop start. loop = " + (loop));
            innerFutureAndOutFuture();
            System.out.println("loop end. loop = " + (loop++));
            Thread.sleep(10);
        }
    }

    public static void innerFutureAndOutFuture() throws ExecutionException, InterruptedException {
        Callable<String> innerCallable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(100);
                return "inner callable";
            }
        };

        Callable<String> outerCallable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(10);
                Future<String> innerFuture = executor.submit(innerCallable);
                String innerResult = innerFuture.get();
                Thread.sleep(10);
                return "outer callable. inner result = " + innerResult;
            }
        };

        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            System.out.println("submit : " + i);
            Future<String> outerFuture = executor.submit(outerCallable);
            futures.add(outerFuture);
        }
        for (int i = 0; i < 10; i++) {
            String outerResult = futures.get(i).get();
            System.out.println(outerResult + ":" + i);
        }
    }
}

查看上述代码, 我们先来梳理下线程池提交任务后的状态

我们先来线程池三个核心参数

  • 核心线程数: 5
  • 最大线程数: 5
  • 阻塞队列: 无界队列

1、for 循环中添加执行 outerCallable, 添加十个任务

这个时候, 核心线程数五个任务已经跑满, 剩余的五个任务会添加到无界队列中

2、而在 outerCallable 中会创建 innerCallable 任务, 提交同一个线程池

因为 innerCallable 是在 outerCallable 的 call 方法中执行的, 相当于也会将十个任务在线程池中执行

3、因为在 outerCallable 任务执行前睡眠了 10 ms, 能够保证 outerCallable 全部得到执行

图1 运行流程

我们先来看一下程序的运行结果

    /**
     * loop start. loop = 0
     * submit : 0
     * submit : 1
     * submit : 2
     * submit : 3
     * submit : 4
     * submit : 5
     * submit : 6
     * submit : 7
     * submit : 8
     * submit : 9
     * java.util.concurrent.ThreadPoolExecutor@5b6dc686[Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 0]
     */

结合运行结果和运行流程图, 我们来分析下, 并提出一些问题

1、outerCallable、innerCallable 各自提交了十次, 为什么 queued tasks 里只有十个任务

想一下哈: 我们核心线程是五个, 根据代码看会提交二十个任务, 理论上不应该在阻塞队列中有十五个任务么?

我们在 for 循环中执行了十次提交 outerCallable, outerCallable 内部执行了 10ms 的睡眠

这时线程池的状态为: 核心线程跑满五个, 阻塞队列任务五个, 此时五个核心线程运行的 outerCallable 从 10 ms 睡眠醒过来

这个时候会通过占有五个核心线程的 outerCallable 执行提交任务 innerCallable

因为只有五个核心线程, 所以阻塞队列中的十个任务是 五个 outerCallable 和五个 innerCallable

2、为什么会导致线程池死锁

通过上面的讲解, 答案已经出来了

五个核心线程运行的 outerCallable 在等待在阻塞队列中的 innerCallable 返回结果

达到了死锁的产生标准, 资源互相等待执行

如何预防线程池死锁

核心点在于: 不要在线程池里等待另一个在池里执行的任务

  • 不要在线程池中的同步方法执行异步任务
  • 不要在异步方法中阻塞等待异步方法

当然, 上面代码死锁的问题, 通过增大线程池线程数也能解决

可以把核心线程数改为 40, 最大线程数改为 50, 看看还能不能死锁

这种解决思路固然能解决, 但是最好还是不要有

因为线程池的参数设置要合理化, 不能为了解决某类问题而设置

文末总结

这里以一个小例子描述了线程池可能发生死锁一种可能

希望大家能通过上述的描述, 能够在项目中避免线程池死锁的可能

参考资料

线程池使用不当也会死锁: https://www.cnblogs.com/caoshenglu/p/9461567.html

 

死锁: https://baike.baidu.com/item/%E6%AD%BB%E9%94%81/2196938?fr=aladdin

点赞收藏
马丁玩编程

公众号:马丁玩编程

请先登录,查看3条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

随机一门技术分享之Netty

随机一门技术分享之Netty

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

5
3