对比 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
调节时钟。