system.nanotime()最准,因其基于单调递增高精度计时器,不受系统时钟调整影响;应在线程内部起止处调用,避免测调度开销,多线程需countdownlatch同步起点,线程池任务宜用future结合内部nanotime()分离排队与执行时间,并注意jit预热和gc干扰。

用 System.nanoTime() 测单个线程执行时间最准
毫秒级的 System.currentTimeMillis() 在多线程场景下容易受系统时钟调整影响,导致负数或跳变;而 System.nanoTime() 基于单调递增的高精度计时器,不受系统时间修改干扰,适合测量耗时。
实操建议:
- 在目标线程内部起始位置调用
long start = System.nanoTime(); - 在结束位置调用
long end = System.nanoTime();,再计算(end - start) / 1_000_000.0得到毫秒值(保留小数更利于观察微小差异) - 避免把
nanoTime()调用放在Thread.start()之前或join()之后——那测的是调度开销,不是线程体真实执行时间
多个线程统一计时要用 CountDownLatch + 共享开始时间戳
如果想对比多个线程从“同一时刻”启动到各自完成的耗时,不能让每个线程自己调用 System.nanoTime() 获取起点——线程调度有延迟,起点实际不同。
正确做法是:主线程预设开始时间,用 CountDownLatch 同步所有工作线程的启动时机:
立即学习“Java免费学习笔记(深入)”;
long startTime = System.nanoTime();
CountDownLatch latch = new CountDownLatch(1);
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 3; i++) {
threads.add(new Thread(() -> {
try { latch.await(); } catch (InterruptedException e) { return; }
long start = System.nanoTime(); // 所有线程此时才真正开始计时
// ... 执行任务
long cost = System.nanoTime() - start;
System.out.println("Thread " + Thread.currentThread().getId() + ": " + cost / 1_000_000.0 + "ms");
}));
}
threads.forEach(Thread::start);
latch.countDown(); // 统一放行
threads.forEach(t -> { try { t.join(); } catch (InterruptedException e) {} });
用 ExecutorService + Future 测异步任务总耗时和各任务耗时
实际项目中多用线程池而非裸线程,这时推荐封装任务为 Callable,利用 Future.get() 的阻塞特性自然捕获执行完成时间。
关键点:
- 主线程调用
submit()后立刻记录开始时间,get()返回时记录结束时间,得到该任务端到端耗时(含排队+执行) - 若要单独看“纯执行时间”,需在
Callable.call()内部用System.nanoTime()计时,并把结果作为返回值的一部分 - 注意
Future.get(long, TimeUnit)可能抛出TimeoutException,需显式处理,否则会掩盖超时问题
别忽略 JIT 预热和 GC 干扰
刚启动的 JVM 中,短小方法可能还没被 JIT 编译,首次执行慢;频繁创建对象又可能触发 GC,导致某次测量突然飙升几十毫秒——这些都不是业务逻辑的真实开销。
实战建议:
- 做性能测试前,先用相同逻辑空跑 1~2 万次(warm-up),让热点代码被编译、类被加载、缓存被填充
- 用
-XX:+PrintGCDetails观察日志,确认测量期间没发生 full GC - 单次测量不可信,应运行多次(如 100 次),取中位数或 p95 值,比平均值更能反映典型表现
线程时间测量真正难的不是写那两行 nanoTime(),而是确保你测的确实是想测的那个“时间”——启动时机、上下文切换、JIT、GC、锁竞争,全都会悄悄掺进去。不控制变量,数字再精确也没意义。







