ThreadLocal与StopWatch结合统计代码运行时间

发布时间 2023-12-08 16:22:55作者: 木马不是马

StopWatch

是springframewrk框架当中用于计时的一个秒表工具类,是线程不安全的,注意不要在多个线程同时使用,会造成计时结果不准确,
最简答的用法如下:

StopWatch stopWatch = new StopWatch();
        stopWatch.start("任务一");
        TimeUnit.MILLISECONDS.sleep(500);
        stopWatch.stop();

        stopWatch.start("任务二");
        TimeUnit.MILLISECONDS.sleep(300);
        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());


在springBoot项目启动的时候会输出 Started in xxx seconds,也是使用的这个StopWatch小组件来计时的

在内部实现也是非常的简单

整的stopwatch的id,以及内部类TaskInfo包含的LinkedList用来记录历史任务信息,每个任务名称以及耗时时间,lastTaskInfo为结束的上一个任务信息,当调用stop之后会重新赋值,keepTaskList 为布尔值,可以理解为是否记录历史的任务信息,
如果为true,则只能获取到当前任务的耗时时间,且不会往taskList记录信息,

使用StopWatch的好处就是,可以减少自定义计时时间,减少long start = System.currentTimeMillis(); end2 - end1,这样的计算方式;
让主业务的逻辑代码更清晰,同时可以根据参数时候记录每个任务的耗时占比

需要注意的地方,StopWatch内部计时非线程安全,避免多个线程同时计时,会导致totalTimeNanos以及taskCount计算混乱;
避免过度使用,个人认为,执行逻辑只有一个任务时,可以没有必要用这个,以往的System.currentTimeMillis()可能更醒目,

但是如果多个任务执行发下使用StopWatch的时候,多个不同的处理逻辑使用stopwatch会将这个变量传过来传过去,造成混乱,增加代码的复杂度,所以结合threadLocal进行计时

/**
 * 运行时间工具类
 */
public class WatchUtils {

    private static ThreadLocal<CustomStopWatch> threadLocal = ThreadLocal.withInitial(() -> new CustomStopWatch());

    private static class CustomStopWatch extends StopWatch {

        private boolean keepTaskList = true;

        @Override
        public void setKeepTaskList(boolean keepTaskList) {
            super.setKeepTaskList(keepTaskList);
            this.keepTaskList = keepTaskList;
        }

        @Override
        public String prettyPrint() {
            StringBuilder sb = new StringBuilder(shortSummary());
            sb.append('\n');
            if (!this.keepTaskList) {
                sb.append("No task info kept");
            }
            else {
                sb.append("---------------------------------------------\n");
                sb.append("ms         %     Task name\n");
                sb.append("---------------------------------------------\n");
                NumberFormat nf = NumberFormat.getNumberInstance();
                nf.setMinimumIntegerDigits(4);
                nf.setGroupingUsed(false);
                NumberFormat pf = NumberFormat.getPercentInstance();
                pf.setMinimumIntegerDigits(3);
                pf.setGroupingUsed(false);
                for (TaskInfo task : getTaskInfo()) {
                    sb.append(nf.format(TimeUnit.NANOSECONDS.toMillis(task.getTimeNanos()))).append("  ");
                    sb.append(pf.format((double) task.getTimeNanos() / getTotalTimeNanos())).append("  ");
                    sb.append(task.getTaskName()).append("\n");
                }
            }
            return sb.toString();
        }
    }

    /**
     * 开始执行时间
     */
    public static void start(String taskName) {
        Assert.notNull(taskName, () -> "taskName不能为空");
        CustomStopWatch stopWatch = threadLocal.get();
        if (stopWatch.currentTaskName() != null) {
            stopWatch.stop();
        }
        stopWatch.start(taskName);
    }

    public static void stop() {
        CustomStopWatch stopWatch = threadLocal.get();
        if (stopWatch.currentTaskName() != null) {
            stopWatch.stop();
        }
    }

    public static String prettyPrint() {
        CustomStopWatch stopWatch = threadLocal.get();
        if (stopWatch.currentTaskName() != null) {
            stopWatch.stop();
        }
        threadLocal.remove();
        return stopWatch.prettyPrint();
    }

    public static String printCurrentTask() {
        CustomStopWatch stopWatch = threadLocal.get();
        if (stopWatch.getLastTaskInfo() != null) {
            StopWatch.TaskInfo taskInfo = stopWatch.getLastTaskInfo();
            return String.format("%s 用时 %sms", taskInfo.getTaskName(), taskInfo.getTimeMillis());
        }
        return "";
    }
}

使用起来也是特别的方便,并且无需手动stop()

因为低版本StopWatch内部使用的是毫秒,但是高版本使用的是更精确的纳秒,所以这里集成了SwtopWatch,重写了打印方法,如果是低版本的,则直接使用StopWatch,如果项目引入了Hutool。可以直接换成Hutool的StopWatch
因为在其内部重载了prettyPrint方法,加入了时间单位