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

简介: JDK 线程池使用不当引发的饥饿死锁问题

在这里插入图片描述

01、前言

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

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

02、什么是死锁

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

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

死锁定义来源: 死锁-百度百科

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

我们通过一个 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 对象锁

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

03、线程池死锁产生的条件

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

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

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(5, 5, 1000, 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 返回结果

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

04、如何预防线程池死锁

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

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

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

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

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

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

05、文末总结

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

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

微信搜索【源码兴趣圈】,关注龙台,回复【资料】领取涵盖 GO、Netty、SpringCLoud Alibaba、Seata、开发规范、面试宝典、数据结构等电子书 or 视频学习资料!
在这里插入图片描述

01、前言

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

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

02、什么是死锁

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

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

死锁定义来源: 死锁-百度百科

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

我们通过一个 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 对象锁

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

03、线程池死锁产生的条件

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

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

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(5, 5, 1000, 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 返回结果

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

04、如何预防线程池死锁

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

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

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

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

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

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

05、文末总结

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

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

相关文章
|
1月前
|
Java 数据库连接 数据库
不同业务使用同一个线程池发生死锁的技术探讨
【10月更文挑战第6天】在并发编程中,线程池是一种常用的优化手段,用于管理和复用线程资源,减少线程的创建和销毁开销。然而,当多个不同业务场景共用同一个线程池时,可能会引发一系列并发问题,其中死锁就是最为严重的一种。本文将深入探讨不同业务使用同一个线程池发生死锁的原因、影响及解决方案,旨在帮助开发者避免此类陷阱,提升系统的稳定性和可靠性。
49 5
|
1月前
|
Dubbo Java 应用服务中间件
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
109 0
剖析Tomcat线程池与JDK线程池的区别和联系!
|
27天前
|
监控 数据可视化 Java
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
|
1月前
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
44 0
|
2月前
|
监控 数据可视化 Java
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
|
3月前
|
缓存 Java 调度
【Java 并发秘籍】线程池大作战:揭秘 JDK 中的线程池家族!
【8月更文挑战第24天】Java的并发库提供多种线程池以应对不同的多线程编程需求。本文通过实例介绍了四种主要线程池:固定大小线程池、可缓存线程池、单一线程线程池及定时任务线程池。固定大小线程池通过预设线程数管理任务队列;可缓存线程池能根据需要动态调整线程数量;单一线程线程池确保任务顺序执行;定时任务线程池支持周期性或延时任务调度。了解并正确选用这些线程池有助于提高程序效率和资源利用率。
52 2
|
3月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
77 6
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
86 5
|
3月前
|
算法 Java
JDK版本特性问题之想控制 G1 垃圾回收器的并行工作线程数量,如何解决
JDK版本特性问题之想控制 G1 垃圾回收器的并行工作线程数量,如何解决
|
3月前
|
Java
Java多线程-死锁的出现和解决
死锁是指多线程程序中,两个或以上的线程在运行时因争夺资源而造成的一种僵局。每个线程都在等待其中一个线程释放资源,但由于所有线程都被阻塞,故无法继续执行,导致程序停滞。例如,两个线程各持有一把钥匙(资源),却都需要对方的钥匙才能继续,结果双方都无法前进。这种情况常因不当使用`synchronized`关键字引起,该关键字用于同步线程对特定对象的访问,确保同一时刻只有一个线程可执行特定代码块。要避免死锁,需确保不同时满足互斥、不剥夺、请求保持及循环等待四个条件。