父子任务使用不当线程池死锁怎么解决?

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 在Java多线程编程中,线程池有助于提升性能与资源利用效率,但若父子任务共用同一池,则可能诱发死锁。本文通过一个具体案例剖析此问题:在一个固定大小为2的线程池中,父任务直接调用`outerTask`,而`outerTask`再次使用同一线程池异步调用`innerTask`。理论上,任务应迅速完成,但实际上却超时未完成。经由`jstack`输出的线程调用栈分析发现,线程陷入等待状态,形成“死锁”。原因是子任务需待父任务完成,而父任务则需等待子任务执行完毕以释放线程,从而相互阻塞。此问题在测试环境中不易显现,常在生产环境下高并发时爆发,重启或扩容仅能暂时缓解。

引言

在Java多线程编程中,线程池是提高性能和资源利用率的常用工具。然而,当父子任务使用同一线程池时,可能导致潜在的死锁问题。本文将深入分析一个实际案例,阐述为何这种设计可能引发死锁,以及如何排查这类问题。

案例背景

考虑以下的伪代码,展示了一个可能导致死锁的场景:

java

代码解读

复制代码


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Scratch {
    private static final ExecutorService pool1 = Executors.newFixedThreadPool(2);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 50; i++) {
            pool1.submit(() -> {
                // 一些任务逻辑
                outerTask();
            });
        }
        try {
            boolean allDone = pool1.awaitTermination(10000, TimeUnit.MILLISECONDS);
            if (allDone) {
                System.out.println("任务完成!");
            } else {
                System.err.println("任务超时未完成!");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private static void outerTask() {
        Future<?> future = pool1.submit(() -> {
            innerTask();
        });
        try {
            // 获取结果
            future.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void innerTask() {
        // 一些任务逻辑
    }
}

简单解释下这个代码, 我们有一个固定线程数大小为2的线程池, 然后向线程池提交任务, 这个任务直接调用outerTask, 这个outerTask不做任何事情, 只通过线程池异步调用innerTask, 但是注意这里使用了同一个线程池提交innerTask.

最后通过awaitTermination等待线程池执行完毕线程终止就结束, 设置了超时10s, 如果任务都完成了打印"任务完成"否则打印"任务超时未完成", 而由于outerTask和innerTask内部都没有其他逻辑, 理论上应该是很快执行完毕, 打印"任务完成", 但实际如何呢, 执行一下, 结果是:

代码解读

复制代码

任务超时未完成!

好, 这是肯定的😳. 那我们分析下为什么? 这是一个线程故障因此首先想到通过jstack打印堆栈分析:

看到的线程调用栈为:

php

代码解读

复制代码

"pool-1-thread-1@852" tid=0x19 nid=NA waiting
  java.lang.Thread.State: WAITING
	  at jdk.internal.misc.Unsafe.park(Unsafe.java:-1)
	  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:221)
	  at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:500)
	  at java.util.concurrent.FutureTask.get(FutureTask.java:190)
	  at Scratch.outerTask(scratch_18.java:32) // 注意这里
	  at Scratch.lambda$main$0(scratch_18.java:11)
	  at Scratch$$Lambda$14/0x00000008010029f0.run(Unknown Source:-1)
	  at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:577)
	  at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:317)
	  at java.util.concurrent.FutureTask.run(FutureTask.java:-1)
	  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	  at java.lang.Thread.run(Thread.java:1589)

可以看到大量pool-1-thread-1开头线程阻塞在了outerTask提交任务的地方, 同时通过查看线程池的workQueue对象可以看到有很多任务堆积:

原因分析

子任务需要等待父任务完成,而父任务内部的子任务通过同一个线程池提交,又需要等待线程池有空闲线程才能得到执行,但父任务需要等待子任务执行完才能执行完毕释放出空闲线程, 陷入了“死锁”。

但在测试环境中可能无法发现,只要线程池线程数量够多,测试环境的并发请求数不够是发现不了这个问题的,只有并发请求数量足够才可能触发而这往往是上到生产环境才可能发生了,通常会造成严重事故,重启或者扩容后在一定时间内看上去恢复正常了但过不久可能又会出现阻塞情况(在我的公司实际发生过这种故障,开发不停重启和扩容但过一段时间仍然会发生这个问题,排查了很长时间才发现问题原因)

解决方案

为避免父子任务使用同一线程池造成死锁,可以考虑使用独立线程池:将父任务和子任务分别提交到不同的线程池,避免共享线程池资源,减少死锁的可能性。

java

代码解读

复制代码

private static final ExecutorService parentPool = Executors.newFixedThreadPool(1);
private static final ExecutorService childPool = Executors.newFixedThreadPool(1);

总结

作为第一篇文章,这个故障实际非常基础,但却十分值得注意,因为这个故障很常见而且容易被误导为机器数量不够导致重启或扩容后依然无法恢复。


本文转载自:https://juejin.cn/post/7329699704095342603

相关文章
|
1月前
|
缓存 负载均衡 Java
c++写高性能的任务流线程池(万字详解!)
本文介绍了一种高性能的任务流线程池设计,涵盖多种优化机制。首先介绍了Work Steal机制,通过任务偷窃提高资源利用率。接着讨论了优先级任务,使不同优先级的任务得到合理调度。然后提出了缓存机制,通过环形缓存队列提升程序负载能力。Local Thread机制则通过预先创建线程减少创建和销毁线程的开销。Lock Free机制进一步减少了锁的竞争。容量动态调整机制根据任务负载动态调整线程数量。批量处理机制提高了任务处理效率。此外,还介绍了负载均衡、避免等待、预测优化、减少复制等策略。最后,任务组的设计便于管理和复用多任务。整体设计旨在提升线程池的性能和稳定性。
79 5
|
1月前
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
44 0
|
3月前
|
前端开发 JavaScript 大数据
React与Web Workers:开启前端多线程时代的钥匙——深入探索计算密集型任务的优化策略与最佳实践
【8月更文挑战第31天】随着Web应用复杂性的提升,单线程JavaScript已难以胜任高计算量任务。Web Workers通过多线程编程解决了这一问题,使耗时任务独立运行而不阻塞主线程。结合React的组件化与虚拟DOM优势,可将大数据处理等任务交由Web Workers完成,确保UI流畅。最佳实践包括定义清晰接口、加强错误处理及合理评估任务特性。这一结合不仅提升了用户体验,更为前端开发带来多线程时代的全新可能。
81 1
|
3月前
|
存储 监控 Java
|
3月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
78 6
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
87 5
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
86 3
|
4月前
|
Java Linux
Java演进问题之1:1线程模型对于I/O密集型任务如何解决
Java演进问题之1:1线程模型对于I/O密集型任务如何解决
|
3月前
|
Cloud Native Java 调度
项目环境测试问题之线程同步器会造成执行完任务的worker等待的情况如何解决
项目环境测试问题之线程同步器会造成执行完任务的worker等待的情况如何解决
|
3月前
|
Java
Java多线程-死锁的出现和解决
死锁是指多线程程序中,两个或以上的线程在运行时因争夺资源而造成的一种僵局。每个线程都在等待其中一个线程释放资源,但由于所有线程都被阻塞,故无法继续执行,导致程序停滞。例如,两个线程各持有一把钥匙(资源),却都需要对方的钥匙才能继续,结果双方都无法前进。这种情况常因不当使用`synchronized`关键字引起,该关键字用于同步线程对特定对象的访问,确保同一时刻只有一个线程可执行特定代码块。要避免死锁,需确保不同时满足互斥、不剥夺、请求保持及循环等待四个条件。