【非AI】并发环境下死锁检测的实现方案

背景

在复杂的并发系统中,尤其是涉及多线程池嵌套调用的场景,死锁和活锁问题往往难以发现和排查。本文介绍一种基于任务依赖图的检测方案,能够在运行时动态追踪任务调用链,自动识别潜在的循环依赖风险。

核心思想

1. 问题建模

将并发任务调用关系抽象为有向图

  • 节点:每个任务(或执行器/线程池)
  • :任务 A 提交任务 B 执行,则存在边 A → B

如果这个有向图中存在环路,则意味着存在潜在的死锁风险。 如果有超时时间,出现死锁时,会稳定触发超时,退化为饥饿问题,本质还是死锁。实际生产中,可以通过这个指标推断死锁已经发生。

scss 复制代码
TaskA → TaskB → TaskC → TaskA  (环路 = 死锁风险)

2. 检测维度

我们关注两个层面的循环依赖:

维度 检测内容
任务级别 任务 A → B → C → A
执行器级别 线程池 X → Y → X

3. 关键技术点

  1. TransmittableThreadLocal (TTL):跨线程传递任务图数据
  2. Guava Graph:高效的图数据结构和环路检测算法
  3. 懒加载:仅在请求结束时构建和分析图
  4. 线程安全队列:无锁收集任务对

实现方案

整体架构

scss 复制代码
┌─────────────────────────────────────────────────────────┐
│                      请求入口                            │
│                   TaskGraph.init()                       │
└─────────────────────┬───────────────────────────────────┘
                      │
    ┌─────────────────▼─────────────────┐
    │         业务代码执行                │
    │   ParallelExecutor.submit(task)   │
    │         ↓ logForking()            │
    │   记录: parent → child            │
    └─────────────────┬─────────────────┘
                      │
    ┌─────────────────▼─────────────────┐
    │         请求结束                    │
    │   TaskGraph.destroy()             │
    │   - 构建有向图                      │
    │   - 检测环路                        │
    │   - 输出告警日志                    │
    └───────────────────────────────────┘

核心数据结构:任务图

java 复制代码
/**
 * 任务依赖图 - 通过 TTL 实现请求级别共享
 */
public class TaskGraph extends TransmittableThreadLocal<TaskGraph.Data> {

    private static final TaskGraph TTL = new TaskGraph();

    /** 任务名 → 执行器名 的映射 */
    static final Map<String, String> TASK_TO_EXECUTOR_MAP = buildTaskExecutorMapping();

    /**
     * 请求级别的图数据(线程安全,所有子线程共享同一实例)
     */
    public static class Data {
        /** 任务对队列(线程安全) */
        final BlockingQueue<EndpointPair<String>> taskPairs = new LinkedTransferQueue<>();

        /** 懒加载:任务依赖图 */
        @Getter(lazy = true)
        private final Graph<String> taskGraph = buildTaskGraph();

        /** 懒加载:执行器依赖图 */
        @Getter(lazy = true)
        private final Graph<String> executorGraph = buildExecutorGraph();

        /** 懒加载:是否存在任务环路 */
        @Getter(lazy = true)
        private final boolean hasTaskCycle = Graphs.hasCycle(getTaskGraph());

        /** 懒加载:是否存在执行器环路 */
        @Getter(lazy = true)
        private final boolean hasExecutorCycle = Graphs.hasCycle(getExecutorGraph());

        /** 懒加载:是否存在自环 */
        @Getter(lazy = true)
        private final boolean hasSelfLoop = checkSelfLoop();

        private Graph<String> buildTaskGraph() {
            Builder<String> builder = GraphBuilder.directed()
                    .allowsSelfLoops(true)
                    .immutable();
            taskPairs.forEach(builder::putEdge);
            return builder.build();
        }

        private Graph<String> buildExecutorGraph() {
            Builder<String> builder = GraphBuilder.directed()
                    .allowsSelfLoops(true)
                    .immutable();

            getTaskGraph().edges().stream()
                    .map(pair -> EndpointPair.ordered(
                            TASK_TO_EXECUTOR_MAP.getOrDefault(pair.source(), "UNKNOWN"),
                            TASK_TO_EXECUTOR_MAP.getOrDefault(pair.target(), "UNKNOWN")))
                    .forEach(builder::putEdge);
            return builder.build();
        }

        private boolean checkSelfLoop() {
            return getExecutorGraph().edges().stream()
                    .anyMatch(p -> Objects.equals(p.nodeU(), p.nodeV()));
        }
    }

    // ============== TTL 核心方法 ==============

    /**
     * 跨线程拷贝:返回同一 Data 实例(引用共享)
     */
    @Override
    public Data copy(Data parentValue) {
        return parentValue;  // 所有子线程共享同一个 Data
    }

    // ============== 公共 API ==============

    /** 请求开始时初始化 */
    public static void init() {
        TTL.set(new Data());
    }

    /** 记录任务依赖关系 */
    public static void logTaskPair(String parent, String child) {
        Data data = TTL.get();
        if (data != null) {
            parent = StringUtils.defaultString(parent, "ROOT");
            data.taskPairs.add(EndpointPair.ordered(parent, child));
        }
    }

    /** 请求结束时检测并清理 */
    public static void destroy() {
        try {
            Data data = TTL.get();
            if (data != null && !data.taskPairs.isEmpty()) {
                checkAndLog(data);
            }
        } finally {
            TTL.remove();
        }
    }

    private static void checkAndLog(Data data) {
        boolean hasTaskCycle = data.isHasTaskCycle();
        boolean hasExecutorCycle = data.isHasExecutorCycle();
        boolean hasSelfLoop = data.isHasSelfLoop();

        if (hasTaskCycle || hasExecutorCycle || hasSelfLoop) {
            String edges = data.getExecutorGraph().edges().stream()
                    .map(p -> p.source() + " → " + p.target())
                    .collect(Collectors.joining(", "));

            log.warn("Potential deadlock detected: taskCycle={}, executorCycle={}, " +
                     "selfLoop={}, edges=[{}]",
                    hasTaskCycle, hasExecutorCycle, hasSelfLoop, edges);
        }
    }
}

并行执行器集成

java 复制代码
/**
 * 并行执行帮助类 - 在任务提交时记录依赖关系
 */
public class ParallelHelper {

    /**
     * 记录任务分叉(父任务 → 子任务)
     */
    public static void logForking(String childTaskName) {
        String parentTaskName = ThreadContext.getCurrentTaskName();
        TaskGraph.logTaskPair(parentTaskName, childTaskName);
    }

    /**
     * 并行执行任务集合
     */
    public static <T, R> List<R> parallelMap(
            List<T> items,
            Function<T, R> mapper,
            ParallelOptions options,
            ExecutorService executor) {

        if (CollectionUtils.isEmpty(items)) {
            return Collections.emptyList();
        }

        // 记录任务依赖
        logForking(options.getTaskName());

        // 包装任务,传递上下文
        List<Callable<R>> tasks = items.stream()
                .map(item -> wrapWithContext(options.getTaskName(), () -> mapper.apply(item)))
                .collect(toList());

        // 提交执行
        return submitAndWait(tasks, executor, options.getTimeout());
    }
}

任务枚举与执行器映射

java 复制代码
/**
 * 子任务枚举 - 定义任务名与执行器的映射关系
 */
public enum SubTaskEnum {

    // 示例任务定义
    PRICE_CALCULATION("PriceCalculation", "COMPUTE_POOL"),
    INVENTORY_CHECK("InventoryCheck", "IO_POOL"),
    CACHE_REFRESH("CacheRefresh", "IO_POOL"),
    RULE_EVALUATION("RuleEvaluation", "COMPUTE_POOL");

    private final String taskName;
    private final String executorName;

    SubTaskEnum(String taskName, String executorName) {
        this.taskName = taskName;
        this.executorName = executorName;
    }

    /** 构建全局映射表 */
    public static final Map<String, String> TASK_TO_EXECUTOR_MAP =
            Arrays.stream(values())
                    .collect(Collectors.toMap(
                            SubTaskEnum::getTaskName,
                            SubTaskEnum::getExecutorName));
}

请求生命周期集成

RPC框架和常驻线程池任务等也需要考虑,这里不作展示。

java 复制代码
/**
 * 请求拦截器 - 管理 TaskGraph 生命周期
 */
public class RequestInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler) {
        ThreadContext.setCurrentTaskName("REQUEST_ENTRY");
        TaskGraph.init();
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                               HttpServletResponse response,
                               Object handler,
                               Exception ex) {
        // 请求结束:检测并清理
        TaskGraph.destroy();
        ThreadContext.remove();
    }
}

检测效果示例

场景:执行器自环

复制代码
COMPUTE_POOL 的任务同步等待 COMPUTE_POOL 的结果

日志输出:

ini 复制代码
WARN - Potential deadlock detected:
       taskCycle=true, executorCycle=true, selfLoop=true,
       edges=[COMPUTE_POOL → COMPUTE_POOL]

场景:跨执行器环路

复制代码
COMPUTE_POOL → IO_POOL → COMPUTE_POOL

日志输出:

ini 复制代码
WARN - Potential deadlock detected:
       taskCycle=true, executorCycle=true, selfLoop=false,
       edges=[COMPUTE_POOL → IO_POOL, IO_POOL → COMPUTE_POOL]

总结

技术要点

TTL (TransmittableThreadLocal) :用于跨线程传递任务图数据。通过重写 copy() 方法返回同一实例,使得整个请求树中的所有线程共享同一个 Data 对象,从而实现任务依赖关系的统一收集。

Guava Graph :提供高效的图数据结构和环路检测能力。使用 GraphBuilder 构建有向图,通过 Graphs.hasCycle() 方法检测是否存在循环依赖。

懒加载机制 :通过 Lombok 的 @Getter(lazy=true) 注解实现懒加载,仅在请求结束需要检测时才构建图并执行分析,避免了运行时的额外计算开销。

LinkedTransferQueue:作为无锁并发队列,支持多线程同时记录任务对。该队列具有良好的并发性能,写入操作的时间复杂度为 O(1)。

Callable 包装:采用装饰器模式包装原始任务,在任务执行前后维护当前任务名上下文,使得嵌套的子任务能够正确记录父子依赖关系。

注意事项

  • 性能开销:TaskGraph 仅在请求结束时构建图结构,运行时只进行队列写入操作,时间复杂度为 O(1),对业务性能影响极小
  • 误报处理:检测到环路并不意味着必然发生死锁,需要结合线程池的拒绝策略(如 CallerRunsPolicy)和实际业务场景综合分析
  • 采样控制:生产环境建议通过配置开关控制采样率,避免对性能敏感场景产生影响

扩展方向

  • 可视化:将任务图导出为 DOT 格式,使用 Graphviz 等工具渲染成可视化图表,便于直观分析依赖关系
  • 告警集成:接入监控系统,当检测到潜在死锁时自动触发告警通知
  • 历史分析:持久化存储历史检测数据,分析高频出现的环路模式,识别系统性的架构问题

通过这套方案,我们能够在运行时自动发现潜在的死锁风险,将问题从"线上排查"前移到"提前预警",显著降低并发问题的定位成本。

相关推荐
青云计划8 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿9 小时前
Jsoniter(java版本)使用介绍
java·开发语言
Victor3569 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor3569 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
探路者继续奋斗9 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
消失的旧时光-194310 小时前
第十九课:为什么要引入消息队列?——异步系统设计思想
java·开发语言
yeyeye11110 小时前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
A懿轩A10 小时前
【Java 基础编程】Java 面向对象入门:类与对象、构造器、this 关键字,小白也能写 OOP
java·开发语言
SQL必知必会11 小时前
SQL 窗口帧:ROWS vs RANGE 深度解析
数据库·sql·性能优化
Tony Bai11 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php