接下来本文将再前面的基础耗时工具类的基础之上,从0到1写一个支持多线程场景下的耗时统计工具类
上文地址: 封装一个基础的耗时统计工具类
1. 设计思路
1.1 明确思路
首先明确目标:
- 实现一个并发安全的
StopWatch
工具类
主要挑战:
- 并发安全
实现思路:
- 参照
StopWatch
的实现,解决并发问题
1.2 设计思路
到这里,假定大家已经看过了StopWatch
的实现源码(实际上没看过也没啥影响)
- 在
StopWatch
中,通过List<TaskInfo> taskList
来记录每个任务的耗时情况 - 因为它主要应用于单线程场景,所以不存在任务的并行耗时记录的场景,通常是要给任务执行完毕,然后开始记录下一个任务,所以在全局使用
startTimeNanos
表示当前任务的开始时间,当结束记录时,将任务耗时情况写入taskList
列表
基本工作原理如下
从StopWatch的工作原理上,想实现一个并发安全的貌似也不难,我们将List换成Map,支持同时记录多个任务的耗时情况
我们的设计上也相对清晰
- 使用一个并发安全的Map容器(如ConcurrentHashMap)来记录任务的耗时情况
- 开始记录一个任务时,向Map中写入
任务名
+当前时间戳
的键值对 - 结束一个任务时,从Map中获取对应任务的时间戳,与当前时间戳取差值,得到任务的执行耗时,并写回到Map中,这样Map中记录的就是这个任务的耗时时间了
- 耗时分布输出:遍历map,打印结果
2. 实现
2.1 基础实现
下面的源码,可在
com.github.liuyueyi.hhui.trace.test.step.Step3
进行查看
接下来我们按照上面的设计思路,现来实现一个Map版本的StopWatch
定义一个工具类 TraceWatch
,申明两个核心变量 taskName:总任务名
+ taskCost:子任务耗时map
public static class TraceWatch {
/**
* 任务名
*/
private String taskName;
/**
* 子任务耗时时间
*/
private Map<String, Long> taskCost;
public TraceWatch(String taskName) {
this.taskName = taskName;
this.taskCost = new ConcurrentHashMap<>();
}
public TraceWatch() {
this("");
}
}
然后就是实现记录某个任务执行耗时的开始、结束方法
public void start(String task) {
taskCost.put(task, System.currentTimeMillis());
}
public void stop(String task) {
Long last = taskCost.get(task);
if (last == null || last < 946656000L ) {
// last = null -> 兼容没有使用开始,直接调用了结束的场景
// last 存的是耗时而非时间戳 -> 兼容重复调用stop的场景
return;
}
taskCost.put(task, System.currentTimeMillis() - last);
}
最后再实现一个各任务的耗时输出分布 (日志打印基本上验用StopWatch的格式化打印,区别在于这里使用的是毫秒输出)
public void prettyPrint() {
StringBuilder sb = new StringBuilder();
sb.append('\n');
long totalCost = taskCost.values().stream().reduce(0L, Long::sum);
sb.append("TraceWatch '").append(taskName).append("': running time = ").append(totalCost).append(" ms");
sb.append('\n');
if (taskCost.isEmpty()) {
sb.append("No task info kept");
} else {
sb.append("---------------------------------------------\n");
sb.append("ms % Task name\n");
sb.append("---------------------------------------------\n");
NumberFormat pf = NumberFormat.getPercentInstance();
pf.setMinimumIntegerDigits(2);
pf.setMinimumFractionDigits(2);
pf.setGroupingUsed(false);
for (Map.Entry<String, Long> entry : taskCost.entrySet()) {
sb.append(entry.getValue()).append("\t\t");
sb.append(pf.format(entry.getValue() / (double) totalCost)).append("\t\t");
sb.append(entry.getKey()).append("\n");
}
}
System.out.printf("\n---------------------\n%s\n--------------------\n%n", sb);
}
到这里,一个支持多任务耗时并行记录的工具类就实现了,接下来写一个测试用例来验证下效果
private static Random random = new Random();
/**
* 随机睡眠一段时间
*
* @param max
*/
private static void randSleep(String task, int max) {
int sleepMillSecond = random.nextInt(max);
try {
System.out.println(task + "==> 随机休眠 " + sleepMillSecond + "ms");
Thread.sleep(sleepMillSecond);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Test
public void testCost() throws InterruptedException {
TraceWatch traceWatch = new TraceWatch();
traceWatch.start("task1");
randSleep("task1", 200);
traceWatch.stop("task1");
// task2为并行执行任务
new Thread(() -> {
traceWatch.start("task2");
randSleep("task2", 100);
traceWatch.stop("task2");
}).start();
回复