对比 currentTimeMillis 与 nanoTime

简介

在 Java 中有两种获取时间的方式,分别是在 java.lang.System 中的 public static native long currentTimeMillis()public static native long nanoTime()

  • currentTimeMillis() 是返回当前的时间戳,这个时间戳的增长幅度可能由于不同的操作系统而变化。

  • nanoTime() 返回系统当前的纳秒时间,这个时间与系统的时钟并没有关系,是由系统的计时器计算返回。

但是由于这两个方法都是 native 修饰的,为了知其然知其所以然,就需要进入 OpenJdk 的源码中一探究竟。

实现

通过在 System.cJava_java_lang_System_registerNatives 方法中发现,currentTimeMillis 注册的函数为 JVM_CurrentTimeMillis,nanoTime 注册的函数为 JVM_NanoTime

static JNINativeMethod methods[] = {
    {"currentTimeMillis", "()J",              (void *)&JVM_CurrentTimeMillis},
    {"nanoTime",          "()J",              (void *)&JVM_NanoTime},
    {"arraycopy",     "(" OBJ "I" OBJ "II)V", (void *)&JVM_ArrayCopy},
};

#undef OBJ

JNIEXPORT void JNICALL
Java_java_lang_System_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}

接着跟踪进入 jvm.cpp,得知这个函数是根据不同的操作系统底层选择调用的实现,这里使用 os_linux.cpp 中的实现作为研究对象。

JVM_LEAF(jlong, JVM_CurrentTimeMillis(JNIEnv *env, jclass ignored))
  JVMWrapper("JVM_CurrentTimeMillis");
  return os::javaTimeMillis();
JVM_END

JVM_LEAF(jlong, JVM_NanoTime(JNIEnv *env, jclass ignored))
  JVMWrapper("JVM_NanoTime");
  return os::javaTimeNanos();
JVM_END

currentTimeMillis

在 Linux 中 javaTimeMillis 的实现是直接调用操作系统的 gettimeofday 函数实现的。这个函数的作用是获取当前系统的时钟时间,并用参数 timeval 保存。

jlong os::javaTimeMillis() {
  timeval time;
  int status = gettimeofday(&time, NULL);
  assert(status != -1, "linux error");
  return jlong(time.tv_sec) * 1000  +  jlong(time.tv_usec / 1000);
}

http://man7.org/linux/man-pages/man2/gettimeofday.2.html

nanoTime

而 javaTimeNanos 是根据操作系统是否支持 clock_gettime 函数选择调用 clock_gettime,否则调用 gettimeofday。OpenJdk 在调用 clock_gettime 时传递的参数为 CLOCK_MONOTONIC,他的功能是获取从系统启动时单调变化的时间。

jlong os::javaTimeNanos() {
  if (os::supports_monotonic_clock()) {
    struct timespec tp;
    int status = os::Posix::clock_gettime(CLOCK_MONOTONIC, &tp);
    assert(status == 0, "gettime error");
    jlong result = jlong(tp.tv_sec) * (1000 * 1000 * 1000) + jlong(tp.tv_nsec);
    return result;
  } else {
    timeval time;
    int status = gettimeofday(&time, NULL);
    assert(status != -1, "linux error");
    jlong usecs = jlong(time.tv_sec) * (1000 * 1000) + jlong(time.tv_usec);
    return 1000 * usecs;
  }
}

http://man7.org/linux/man-pages/man2/clock_gettime.2.html

总结

那么这两种时钟的获取又有什么区别呢?首先,根据文档可知,gettimeofday 返回的是系统的绝对时间,这个时间会受 Linux 的时钟同步机制影响 ( RFC 5905)。clock_gettime 则是返回经由系统启动后的相对时间,时间的跳动会受到操作系统和所使用计时器的影响,并且是单调递增的。

在实际应用中,currentTimeMillis() 会比 nanoTime() 有更好的作用,例如 new Data() 的底层就是调用了 currentTimeMillis 作为时间参数,而 nanoTime 是意义更多是相对偏移时间,所以在计算耗时这上面会更显突出。

在常见的分布式全局编号 – 雪花算法中,时间戳是保证全局编号递增的关键,然而现实场景下是有可能出现 时钟回拨 现象,那么单调递增的 nanoTime 是否更适合呢?

其实这个问题的解法有很多种,选择适用的即可。

  1. 最常见的解决方法就是记录上一次的时间戳,在新产生的时间戳先于这个时间戳时,那就进行 sleep 后重新获取。

  2. 使用 AtomicLong 代替时间戳来规避时钟回拨。

  3. 使用 nanoTime,由于 nanoTime 是单调递增的,所以也不会出现时钟回拨的现场。但是 nanoTime 是会有产生溢出的可能,导致返回负值。

  4. 使用 adjtime()adjtimex() 设置时钟调节策略,调节系统时钟变化的速率。当同步时间落后当前系统时钟,那系统将会使用一个缓慢的速率,等到时间一致,这样就能够优雅的使用时间戳并防止时钟回拨。但是如果系统时钟落后太多,那么同步则需要过多的耗时。

ntpdchrony 通过设置都能做到避免一定程度的时钟回拨,ntpdata -B 也能强制使用 adjtime 调节时钟。

参考: https://stackoverflow.com/questions/12392278/measure-time-in-linux-time-vs-clock-vs-getrusage-vs-clock-gettime-vs-gettimeof