对比 currentTimeMillis 与 nanoTime
简介
在 Java 中有两种获取时间的方式,分别是在 java.lang.System 中的 public static native long currentTimeMillis() 和 public static native long nanoTime()。
- 
currentTimeMillis() 是返回当前的时间戳,这个时间戳的增长幅度可能由于不同的操作系统而变化。
 - 
nanoTime() 返回系统当前的纳秒时间,这个时间与系统的时钟并没有关系,是由系统的计时器计算返回。
 
但是由于这两个方法都是 native 修饰的,为了知其然知其所以然,就需要进入 OpenJdk 的源码中一探究竟。
实现
通过在 System.c 的 Java_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);
}
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;
  }
}
总结
那么这两种时钟的获取又有什么区别呢?首先,根据文档可知,gettimeofday 返回的是系统的绝对时间,这个时间会受 Linux 的时钟同步机制影响 (
RFC 5905)。clock_gettime 则是返回经由系统启动后的相对时间,时间的跳动会受到操作系统和所使用计时器的影响,并且是单调递增的。
在实际应用中,currentTimeMillis() 会比 nanoTime() 有更好的作用,例如 new Data() 的底层就是调用了 currentTimeMillis 作为时间参数,而 nanoTime 是意义更多是相对偏移时间,所以在计算耗时这上面会更显突出。
在常见的分布式全局编号 – 
雪花算法中,时间戳是保证全局编号递增的关键,然而现实场景下是有可能出现 时钟回拨 现象,那么单调递增的 nanoTime 是否更适合呢?
其实这个问题的解法有很多种,选择适用的即可。
- 
最常见的解决方法就是记录上一次的时间戳,在新产生的时间戳先于这个时间戳时,那就进行 sleep 后重新获取。
 - 
使用
AtomicLong代替时间戳来规避时钟回拨。 - 
使用
nanoTime,由于 nanoTime 是单调递增的,所以也不会出现时钟回拨的现场。但是 nanoTime 是会有产生溢出的可能,导致返回负值。 - 
使用 adjtime() 或 adjtimex() 设置时钟调节策略,调节系统时钟变化的速率。当同步时间落后当前系统时钟,那系统将会使用一个缓慢的速率,等到时间一致,这样就能够优雅的使用时间戳并防止时钟回拨。但是如果系统时钟落后太多,那么同步则需要过多的耗时。
 
ntpd 和 chrony 通过设置都能做到避免一定程度的时钟回拨,
ntpdata -B也能强制使用adjtime调节时钟。