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

核心思想
1. 问题建模
将并发任务调用关系抽象为有向图:
- 节点:每个任务(或执行器/线程池)
- 边:任务 A 提交任务 B 执行,则存在边 A → B
如果这个有向图中存在环路,则意味着存在潜在的死锁风险。 如果有超时时间,出现死锁时,会稳定触发超时,退化为饥饿问题,本质还是死锁。实际生产中,可以通过这个指标推断死锁已经发生。
scss
TaskA → TaskB → TaskC → TaskA (环路 = 死锁风险)
2. 检测维度
我们关注两个层面的循环依赖:
| 维度 | 检测内容 |
|---|---|
| 任务级别 | 任务 A → B → C → A |
| 执行器级别 | 线程池 X → Y → X |
3. 关键技术点
- TransmittableThreadLocal (TTL):跨线程传递任务图数据
- Guava Graph:高效的图数据结构和环路检测算法
- 懒加载:仅在请求结束时构建和分析图
- 线程安全队列:无锁收集任务对
实现方案
整体架构
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 等工具渲染成可视化图表,便于直观分析依赖关系
- 告警集成:接入监控系统,当检测到潜在死锁时自动触发告警通知
- 历史分析:持久化存储历史检测数据,分析高频出现的环路模式,识别系统性的架构问题
通过这套方案,我们能够在运行时自动发现潜在的死锁风险,将问题从"线上排查"前移到"提前预警",显著降低并发问题的定位成本。