StopWatch 使用教程

🚀StopWatch 使用教程

🤔 什么是StopWatch

Hey 大家好,我是 Shio👋。今天我们来学习一个Spring框架中的工具类StopWatchStopWatch是一个简单的停止看门狗计时器,它可以帮助开发者测量代码的执行时间。这对于性能调优监控应用性能非常有用。想象一下,你有一个复杂的业务逻辑,你想知道每一步操作需要多长时间来执行StopWatch就是你的瑞士军刀

首先我们要了解为什么我们会使用StopWatch? 我总结为两点:

  • 方便易用的API
  • 纳秒级别的准确性

当然除了API, 我还想讲一讲StopWatch准确性实现所用到的 System.nanoTime()方法, 以及为什么不用System.currentTimeMillis()

📏StopWatch准确性如何保障?

StopWatch 类在Spring框架中用于精确测量时间间隔,它之所以能够提供高精度的时间测量,很大程度上归功于其内部使用的 System.nanoTime() 方法。下面我们来详细探讨 System.nanoTime() 以及 StopWatch 为何因此而更准确。

System.nanoTime() 的作用

System.nanoTime() 是 Java 中用于获取当前时间的纳秒值的方法。它提供了对最高精度的系统时钟的访问,通常由操作系统的实时时钟或高精度时钟提供。这个方法的返回值是 JVM 启动以来的纳秒数,不考虑闰秒。

  1. 纳秒级精度System.nanoTime() 提供的是纳秒级别的时间,这意味着StopWatch能够测量到非常短的时间间隔,这对于性能分析来说是非常有用的。
  2. 系统时钟直接读取System.nanoTime() 直接读取系统时钟,这减少了由于时间同步或时间转换导致的误差。
  3. 不受用户模式延迟影响:由于它使用的是系统时钟,因此不会受到用户模式下可能发生的延迟的影响,比如线程调度延迟。
  4. 单线程环境下的准确性:在单线程环境中,System.nanoTime() 可以非常精确地测量两个事件之间的时间间隔。
  5. 减少累积误差StopWatch 通过在任务开始和结束时记录时间戳,然后计算差值来测量每个任务的执行时间,这种方法减少了时间测量的累积误差。

System.currentTimeMillis()进行比较

System.currentTimeMillis()System.nanoTime() 都是 Java 中用于获取时间的方法,但它们之间存在一些关键的区别,这些区别决定了它们在不同场景下的适用性

System.currentTimeMillis()

  • 返回值:返回的是自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数,也称为 Unix 时间或 Epoch 时间。
  • 精度:以毫秒为单位,这意味着最小时间间隔为 1 毫秒。
  • 用途:通常用于记录一个具体时间点,或者在不需要高精度时间差测量的场景中。
  • 线程安全:由于每次调用获取的是单个时间戳,因此是线程安全的。
  • 系统时钟依赖:依赖于系统时钟,可能会受到系统时间调整的影响。

System.nanoTime()

  • 返回值:返回的是自 JVM 启动以来的纳秒数,不依赖于日期和时间。
  • 精度:以纳秒为单位,最小时间间隔为 1 纳秒,这使得它非常适合于高精度的时间间隔测量。
  • 用途:主要用于测量短时间的持续时间,比如性能测试和基准测试。
  • 线程安全:同样是线程安全的,因为它每次调用也是获取单个时间戳。
  • 系统时钟依赖:依赖于系统提供的高精度时钟,通常不受系统时间调整的影响。

主要区别

  1. 时间起点System.currentTimeMillis() 以 Unix 时间(1970 年 1 月 1 日 00:00:00 UTC)为起点,而 System.nanoTime()JVM 启动时间为起点。
  2. 时间单位System.currentTimeMillis() 以毫秒为单位,而 System.nanoTime() 以纳秒为单位。
  3. 精度System.nanoTime() 的精度远高于 System.currentTimeMillis(),适合于需要精确测量时间间隔的场景。
  4. 适用场景System.currentTimeMillis() 适合获取具体时间点的记录,而 System.nanoTime() 更适合测量两个事件之间的时间间隔。
  5. 系统调整影响:系统时间的调整可能会影响 System.currentTimeMillis() 的返回值,而 System.nanoTime() 则不受这种调整的影响。

使用建议

  • 当需要记录具体日期和时间点时,使用 System.currentTimeMillis()
  • 当需要测量代码块或方法执行的性能时,使用 System.nanoTime()
  • 在多线程环境中,两者都可以用来获取时间戳,但 System.nanoTime() 更适合于测量短时的持续时间。

结论

StopWatch 之所以能够提供高精度的时间测量,主要得益于其内部使用的 System.nanoTime() 方法。该方法提供了对系统时钟的直接访问,从而允许 StopWatch 以纳秒级别的精度测量时间间隔。然而,开发者在使用时应考虑到多线程环境和系统负载等因素可能带来的影响,并合理地应用 StopWatch 以优化性能分析。

🛠️ 如何使用StopWatch

1. 添加依赖

首先,确保你的项目中已经包含了Spring的上下文模块。在Maven的pom.xml文件中添加以下依赖:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10</version>
</dependency>

2. 初始化StopWatch

在你的Spring配置文件或者Java配置类中,你可以注入StopWatch,并开始使用它:

import org.springframework.util.StopWatch;

public class PerformanceTest {
private final StopWatch stopWatch = new StopWatch();

public void testPerformance() {
stopWatch.start("TaskName"); // 开始计时,并给任务命名
// 执行你的代码
stopWatch.stop(); // 停止计时
stopWatch.start("AnotherTask"); // 开始另一个任务的计时
// 执行另一个代码块
stopWatch.stop(); // 停止另一个任务的计时
}
}

3. 获取和打印结果

StopWatch提供了多种方法来获取计时结果。你可以获取总的运行时间,也可以获取每个任务的运行时间:

public void printResults() {
stopWatch.start("TotalTasks");
performanceTask(); // 假设这是你的业务方法
stopWatch.stop();

System.out.println("Total time taken: " + stopWatch.getTotalTimeMillis() + " ms");

for (int i = 0; i < stopWatch.getTaskCount(); i++) {
System.out.println("Task " + i + " took: " + stopWatch.getTaskInfo(i).getDuration() + " ms");
}
}


🌟源码解析

类变量和构造方法

StopWatch类有一些类变量和一个构造方法:

public class StopWatch {
private final String id;
private boolean keepTaskList;
private final List<TaskInfo> taskList;
private long startTimeNanos;
@Nullable
private String currentTaskName;
@Nullable
private TaskInfo lastTaskInfo;
private int taskCount;
private long totalTimeNanos;

//....
}
  • id: 一个字符串,用于标识这个StopWatch实例。
  • keepTaskList: 一个布尔值,指示是否保留任务列表。
  • taskList: 一个任务信息列表,用于存储每个任务的详细信息。
  • startTimeNanos: 当前任务的开始时间(纳秒)。
  • currentTaskName: 当前正在执行的任务名称。
  • lastTaskInfo: 上一个任务的信息。
  • taskCount: 任务计数器。
  • totalTimeNanos: 所有任务的总时间(纳秒)。
public StopWatch() {
this("");
}

public StopWatch(String id) {
this.keepTaskList = true;
this.taskList = new ArrayList(1);
this.id = id;
}

构造方法允许你为StopWatch实例指定一个ID,如果不指定,则默认为空字符串

核心方法

start()

public void start() throws IllegalStateException {
this.start("");
}

public void start(String taskName) throws IllegalStateException {
if (this.currentTaskName != null) {
throw new IllegalStateException("Can't start StopWatch: it's already running");
} else {
this.currentTaskName = taskName;
this.startTimeNanos = System.nanoTime();
}
}

start() 方法用于开始一个新的计时任务。它接受一个任务名称作为参数,并记录当前时间(以纳秒为单位)作为开始时间。如果尝试在另一个任务正在进行时开始新任务,将抛出IllegalStateException

stop()

public void stop() throws IllegalStateException {
if (this.currentTaskName == null) {
throw new IllegalStateException("Can't stop StopWatch: it's not running");
} else {
long lastTime = System.nanoTime() - this.startTimeNanos;
this.totalTimeNanos += lastTime;
this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime);
if (this.keepTaskList) {
this.taskList.add(this.lastTaskInfo);
}

++this.taskCount;
this.currentTaskName = null;
}
}

stop() 方法用于结束当前计时任务。它计算当前时间与开始时间的差值,并将这个时间累加到总时间中。如果尝试停止一个未开始的任务,将抛出IllegalStateException

isRunning()

 public boolean isRunning() {
return this.currentTaskName != null;
}

返回一个布尔值,指示是否有任务正在进行。

getLastTaskTimeNanos() / getLastTaskTimeMillis()

public long getLastTaskTimeNanos() throws IllegalStateException {
if (this.lastTaskInfo == null) {
throw new IllegalStateException("No tasks run: can't get last task interval");
} else {
return this.lastTaskInfo.getTimeNanos();
}
}

public long getLastTaskTimeMillis() throws IllegalStateException {
if (this.lastTaskInfo == null) {
throw new IllegalStateException("No tasks run: can't get last task interval");
} else {
return this.lastTaskInfo.getTimeMillis();
}
}

这些方法返回最后一个任务的执行时间,可以是纳秒毫秒

getTotalTimeNanos() / getTotalTimeMillis() / getTotalTimeSeconds()

public long getTotalTimeNanos() {
return this.totalTimeNanos;
}

public long getTotalTimeMillis() {
return nanosToMillis(this.totalTimeNanos);
}

public double getTotalTimeSeconds() {
return nanosToSeconds(this.totalTimeNanos);
}

这些方法返回所有任务的总执行时间,可以是纳秒、毫秒或秒。

getTaskInfo()

public TaskInfo[] getTaskInfo() {
if (!this.keepTaskList) {
throw new UnsupportedOperationException("Task info is not being kept!");
} else {
return (TaskInfo[])this.taskList.toArray(new TaskInfo[0]);
}
}

如果keepTaskListtrue,此方法返回一个包含所有任务信息的数组。

shortSummary() / prettyPrint() / toString()

public String shortSummary() {
return "StopWatch '" + this.getId() + "': running time = " + this.getTotalTimeNanos() + " ns";
}

public String prettyPrint() {
StringBuilder sb = new StringBuilder(this.shortSummary());
sb.append('\n');
if (!this.keepTaskList) {
sb.append("No task info kept");
} else {
sb.append("---------------------------------------------\n");
sb.append("ns % Task name\n");
sb.append("---------------------------------------------\n");
NumberFormat nf = NumberFormat.getNumberInstance();
nf.setMinimumIntegerDigits(9);
nf.setGroupingUsed(false);
NumberFormat pf = NumberFormat.getPercentInstance();
pf.setMinimumIntegerDigits(3);
pf.setGroupingUsed(false);
TaskInfo[] var4 = this.getTaskInfo();
int var5 = var4.length;

for(int var6 = 0; var6 < var5; ++var6) {
TaskInfo task = var4[var6];
sb.append(nf.format(task.getTimeNanos())).append(" ");
sb.append(pf.format((double)task.getTimeNanos() / (double)this.getTotalTimeNanos())).append(" ");
sb.append(task.getTaskName()).append("\n");
}
}

return sb.toString();
}

public String toString() {
StringBuilder sb = new StringBuilder(this.shortSummary());
if (this.keepTaskList) {
TaskInfo[] var2 = this.getTaskInfo();
int var3 = var2.length;

for(int var4 = 0; var4 < var3; ++var4) {
TaskInfo task = var2[var4];
sb.append("; [").append(task.getTaskName()).append("] took ").append(task.getTimeNanos()).append(" ns");
long percent = Math.round(100.0 * (double)task.getTimeNanos() / (double)this.getTotalTimeNanos());
sb.append(" = ").append(percent).append("%");
}
} else {
sb.append("; no task info kept");
}

return sb.toString();
}

这些方法提供了不同格式的StopWatch状态的字符串表示,包括简短的摘要、详细的打印信息和字符串表示。

辅助方法

nanosToMillis() / nanosToSeconds()

private static long nanosToMillis(long duration) {
return TimeUnit.NANOSECONDS.toMillis(duration);
}

private static double nanosToSeconds(long duration) {
return (double)duration / 1.0E9;
}

这些是私有静态方法,用于将纳秒转换为毫秒或秒。

内部类 TaskInfo

public static final class TaskInfo {
private final String taskName;
private final long timeNanos;

TaskInfo(String taskName, long timeNanos) {
this.taskName = taskName;
this.timeNanos = timeNanos;
}

public String getTaskName() {
return this.taskName;
}

public long getTimeNanos() {
return this.timeNanos;
}

public long getTimeMillis() {
return StopWatch.nanosToMillis(this.timeNanos);
}

public double getTimeSeconds() {
return StopWatch.nanosToSeconds(this.timeNanos);
}
}

TaskInfoStopWatch的内部类,用于存储单个任务的名称和执行时间(纳秒)。它提供了获取任务名称、时间(纳秒、毫秒或秒)的方法。

使用StopWatch的最佳实践

  • 使用start()stop()成对出现,以确保时间的准确性。
  • 通过setKeepTaskList()方法可以控制是否保留任务列表,这有助于节省内存,尤其是在测量大量任务时。
  • 使用prettyPrint()方法可以方便地查看所有任务的执行时间及其占比。

🚧 注意事项

  • 不要在代码的每个角落都使用StopWatch,这可能会导致性能开销。
  • 使用StopWatch的最佳实践是在开发和测试阶段进行性能分析,而不是在生产环境中频繁使用。
  • 对于性能测试,虽然StopWatch可以进行单一有效的测试, 但是还是建议使用如 JMH (Java Microbenchmarking Harness)JMeterGatling 等更加专业高效, 具有丰富测试场景的性能测试框架

🎉 结语

StopWatch是一个简单但强大的工具,可以帮助你监控和优化Spring应用的性能。记住,性能调优是一个持续的过程,不断地使用StopWatch来分析你的代码,你会发现性能提升的空间,让你的应用更加高效💨。


希望这篇教程能帮助你在Spring应用中有效地使用StopWatch工具类。如果你有任何问题或者想要了解更多关于Spring框架的内容,随时欢迎提问!💬