背景
在并发编程中,我们经常需要将一个大任务拆分成多个子任务并行执行。但随之而来的问题是:如何准确统计每个子任务的耗时?
传统的做法是在业务代码中手动埋点,但这样会导致代码侵入性强、难以维护。本文介绍一种基于装饰器模式的优雅实现方案。
核心思路
通过包装 Callable 接口,在任务执行前后自动记录时间戳,实现对任务生命周期各阶段的监控:
lua
Timeline: submitTime -> startTime -> endTime
| | |
+-- 等待时间 --+-- 执行时间 --+
| |
+------- 总耗时 -----------+
代码实现
1. 定义时间记录字段
java
public class ScopedCallable<V> implements Callable<V> {
private static final long NANO_TO_MS = 1_000_000L;
/** 任务名称 */
private final String taskName;
/** 被包装的实际任务 */
private final Callable<V> delegate;
/** 任务提交时间 (纳秒) */
private final long submitTime;
/** 任务开始执行时间 (纳秒) */
private long startTime;
/** 任务执行结束时间 (纳秒) */
private long endTime;
}
2. 在构造函数中记录提交时间
java
public ScopedCallable(String taskName, Callable<V> delegate) {
this.taskName = Objects.requireNonNull(taskName);
this.delegate = Objects.requireNonNull(delegate);
this.submitTime = System.nanoTime(); // 记录提交时间
}
3. 在 call() 方法中记录执行时间
java
@Override
public V call() throws Exception {
try {
startTime = System.nanoTime(); // 记录开始时间
return delegate.call(); // 执行实际任务
} finally {
endTime = System.nanoTime(); // 记录结束时间
reportMetrics(); // 上报监控指标
}
}
4. 计算各阶段耗时
java
/** 执行耗时 = 结束时间 - 开始时间 */
public long executionTime() {
return endTime - startTime;
}
/** 等待耗时 = 开始时间 - 提交时间 */
public long waitTime() {
return startTime - submitTime;
}
/** 总耗时 = 结束时间 - 提交时间 */
public long totalTime() {
return endTime - submitTime;
}
5. 上报监控指标
java
private void reportMetrics() {
long execMs = executionTime() / NANO_TO_MS;
long waitMs = waitTime() / NANO_TO_MS;
long totalMs = totalTime() / NANO_TO_MS;
System.out.println("[" + taskName + "] 执行耗时: " + execMs + "ms");
System.out.println("[" + taskName + "] 等待耗时: " + waitMs + "ms");
System.out.println("[" + taskName + "] 总耗时: " + totalMs + "ms");
}
设计要点
| 要点 | 说明 |
|---|---|
| 纳秒精度 | 使用 System.nanoTime() 而非 currentTimeMillis(),精度更高且不受系统时钟调整影响 |
| finally 块 | 确保即使任务抛异常也能记录结束时间 |
| 透明包装 | 对业务代码无侵入,只需在提交任务时包装一层 |
使用示例
java
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交包装后的任务
Future<String> future = executor.submit(
new ScopedCallable<>("queryDB", () -> {
Thread.sleep(100); // 模拟耗时操作
return "result";
})
);
System.out.println("结果: " + future.get());
executor.shutdown();
}
输出示例:
csharp
[queryDB] 执行耗时: 102ms
[queryDB] 等待耗时: 0ms
[queryDB] 总耗时: 102ms
结果: result
总结
通过装饰器模式包装 Callable,可以优雅地实现:
- 提交时间、等待时间、执行时间 的自动采集
- 监控指标 的自动上报
这种方案的核心优势是零侵入------业务代码无需修改,只需在任务提交处统一包装即可。