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、文末总结

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

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

相关文章
|
5天前
|
Java 测试技术 PHP
父子任务使用不当线程池死锁怎么解决?
在Java多线程编程中,线程池有助于提升性能与资源利用效率,但若父子任务共用同一池,则可能诱发死锁。本文通过一个具体案例剖析此问题:在一个固定大小为2的线程池中,父任务直接调用`outerTask`,而`outerTask`再次使用同一线程池异步调用`innerTask`。理论上,任务应迅速完成,但实际上却超时未完成。经由`jstack`输出的线程调用栈分析发现,线程陷入等待状态,形成“死锁”。原因是子任务需待父任务完成,而父任务则需等待子任务执行完毕以释放线程,从而相互阻塞。此问题在测试环境中不易显现,常在生产环境下高并发时爆发,重启或扩容仅能暂时缓解。
|
10天前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
47 6
|
10天前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
51 5
|
10天前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
42 3
|
4天前
|
算法 Java
JDK版本特性问题之想控制 G1 垃圾回收器的并行工作线程数量,如何解决
JDK版本特性问题之想控制 G1 垃圾回收器的并行工作线程数量,如何解决
|
5天前
|
Java
Java多线程-死锁的出现和解决
死锁是指多线程程序中,两个或以上的线程在运行时因争夺资源而造成的一种僵局。每个线程都在等待其中一个线程释放资源,但由于所有线程都被阻塞,故无法继续执行,导致程序停滞。例如,两个线程各持有一把钥匙(资源),却都需要对方的钥匙才能继续,结果双方都无法前进。这种情况常因不当使用`synchronized`关键字引起,该关键字用于同步线程对特定对象的访问,确保同一时刻只有一个线程可执行特定代码块。要避免死锁,需确保不同时满足互斥、不剥夺、请求保持及循环等待四个条件。
|
20天前
|
消息中间件 算法 Java
(十四)深入并发之线程、进程、纤程、协程、管程与死锁、活锁、锁饥饿详解
本文深入探讨了并发编程的关键概念和技术挑战。首先介绍了进程、线程、纤程、协程、管程等概念,强调了这些概念是如何随多核时代的到来而演变的,以满足高性能计算的需求。随后,文章详细解释了死锁、活锁与锁饥饿等问题,通过生动的例子帮助理解这些现象,并提供了预防和解决这些问题的方法。最后,通过一个具体的死锁示例代码展示了如何在实践中遇到并发问题,并提供了几种常用的工具和技术来诊断和解决这些问题。本文旨在为并发编程的实践者提供一个全面的理解框架,帮助他们在开发过程中更好地处理并发问题。
|
2月前
|
SQL 安全 Java
JUC多线程-线程池-Thredalocal-CAS-AQS-死锁
JUC多线程-线程池-Thredalocal-CAS-AQS-死锁
|
2月前
|
Java
java线程之死锁
java线程之死锁
18 0
|
2月前
|
Java
synchronized关键字在Java中为多线程编程提供了一种简便的方式来管理并发,防止数据竞争和死锁等问题
Java的`synchronized`关键字确保多线程环境中的数据一致性,通过锁定代码段或方法防止并发冲突。它可修饰方法(整个方法为临界区)或代码块(指定对象锁)。例如,同步方法只允许一个线程执行,同步代码块则更灵活,可锁定特定对象。使用时需谨慎,以避免性能影响和死锁。
20 0