Poison

System.nanoTime()

最近查了个关于 System.nanoTime() 的问题,起因是业务里面将 System.nanoTime() 返回的数值作为了业务中的唯一值,最后发现了值相同的数据,询问编写这块代码的同事,同事反馈说当时编写的时候以为 System.nanoTime() 的精度很高,不会出现重复的数据。但是从现象来看,出现了重复的数据。

我们可以用一段简单的代码复现该问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {

private static volatile long a = -1, b = -2;

public static void main(String[] args) {
long max = 1_000_000;
new Thread(() -> {
for (int i = 0; i < max; i++) {
a = System.nanoTime();
}
}).start();
new Thread(() -> {
for (int i = 0; i < max; i++) {
b = System.nanoTime();
}
}).start();
for (int i = 0; i < max; i++) {
if (a == b) {
System.out.println("nanoTime not unique");
}
}
}

}

使用 javac Test.java 编译该类,再使用 java Test 执行以上代码,在 4 核的 Linux 系统上打印了不少 nanoTime not unique。可知,出现了重复的数据。

System.nanoTime() 对应的 Java 源码位于 System.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Returns the current value of the running Java Virtual Machine's
* high-resolution time source, in nanoseconds.
*
* <p>This method can only be used to measure elapsed time and is
* not related to any other notion of system or wall-clock time.
* The value returned represents nanoseconds since some fixed but
* arbitrary <i>origin</i> time (perhaps in the future, so values
* may be negative). The same origin is used by all invocations of
* this method in an instance of a Java virtual machine; other
* virtual machine instances are likely to use a different origin.
*
* <p>This method provides nanosecond precision, but not necessarily
* nanosecond resolution (that is, how frequently the value changes)
* - no guarantees are made except that the resolution is at least as
* good as that of {@link #currentTimeMillis()}.
*
* <p>Differences in successive calls that span greater than
* approximately 292 years (2<sup>63</sup> nanoseconds) will not
* correctly compute elapsed time due to numerical overflow.
*
* <p>The values returned by this method become meaningful only when
* the difference between two such values, obtained within the same
* instance of a Java virtual machine, is computed.
*
* <p> For example, to measure how long some code takes to execute:
* <pre> {@code
* long startTime = System.nanoTime();
* // ... the code being measured ...
* long estimatedTime = System.nanoTime() - startTime;}</pre>
*
* <p>To compare two nanoTime values
* <pre> {@code
* long t0 = System.nanoTime();
* ...
* long t1 = System.nanoTime();}</pre>
*
* one should use {@code t1 - t0 < 0}, not {@code t1 < t0},
* because of the possibility of numerical overflow.
*
* @return the current value of the running Java Virtual Machine's
* high-resolution time source, in nanoseconds
* @since 1.5
*/
public static native long nanoTime();

其中特意提到:

This method provides nanosecond precision, but not necessarily nanosecond resolution (that is, how frequently the value changes) - no guarantees are made except that the resolution is at least as good as that of currentTimeMillis().

即值更改的频率不保证能够达到纳秒解析度。关于这一点,在 Nanotrusting the Nanotime 中也有提及:

nanoTime is not nanosecond resolution; at best, you can hope for 30 ns resolution, and it varies wildly between platforms

System.nanoTime() 方法在 Linux 下的源码实现位于 os_linux.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jlong os::javaTimeNanos() {
if (Linux::supports_monotonic_clock()) {
struct timespec tp;
int status = Linux::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;
}
}

可知,当 Linux 支持单调时钟时,调用的为 clock_gettime 方法,且调用时传入的参数为 CLOCK_MONOTONIC。当不支持单调时钟时,调用的为 gettimeofday 方法。我们使用如下的程序来 debug 下 native 方法:

1
2
3
4
5
6
7
public class TestNative {

public static void main(String[] args) {
System.nanoTime();
}

}

TestNative 使用 gdb debug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(gdb) break os::javaTimeNanos
Breakpoint 1 at 0x7ffff694ebc2: file /tmp/jdk/hotspot/src/os/linux/vm/os_linux.cpp, line 1454.
(gdb) run TestNative
Starting program: /tmp/output-jdk8-dev-dbg/images/j2sdk-image/bin/java TestNative
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7fe9700 (LWP 12106)]
[Switching to Thread 0x7ffff7fe9700 (LWP 12106)]

Breakpoint 1, os::javaTimeNanos () at /tmp/jdk/hotspot/src/os/linux/vm/os_linux.cpp:1454
1454 if (Linux::supports_monotonic_clock()) {
(gdb) n
1456 int status = Linux::clock_gettime(CLOCK_MONOTONIC, &tp);
(gdb) n
1457 assert(status == 0, "gettime error");
(gdb) print status
$1 = 0
(gdb) n
1458 jlong result = jlong(tp.tv_sec) * (1000 * 1000 * 1000) + jlong(tp.tv_nsec);
(gdb) n
1459 return result;
(gdb) print result
$2 = 2064546505326790

确认在我们的生产环境中,调用的为 clock_gettime 方法,关于参数 CLOCK_MONOTONIC,其文档位于 clock_gettime(3) - Linux manual page:

A nonsettable system-wide clock that represents monotonic time since—as described by POSIX—“some unspecified point in the past”. On Linux, that point corresponds to the number of seconds that the system has been running since it was booted.

The CLOCK_MONOTONIC clock is not affected by discontinuous jumps in the system time (e.g., if the system administrator manually changes the clock), but is affected by the incremental adjustments performed by adjtime(3) and NTP. This clock does not count time that the system is suspended. All CLOCK_MONOTONIC variants guarantee that the time returned by consecutive calls will not go backwards, but successive calls may—depending on the architecture—return identical (not-increased) time values.

即持续的调用可能返回相同的值(取决于架构实现)。

Reference

Is System.nanoTime() guaranteed to return unique values? - Stack Overflow
Is the system nanoTime unique? - Quora
Can I trust that System.nanoTime() will return different value each invocation? - Stack Overflow
How to debug a native function in Java - Quora
gdb tutorial