This article explains why it is not possible to make a 1 nanosecond sleep in JVM. Instead, you may achieve an unguaranteed small pause which depends on the number of active threads.
Available options
The JVM offers various techniques to pause or delay a thread for a specified duration:
Thread.yield()
;Thread.sleep(long millis)
andThread.sleep(long millis, int nanos)
;Thread.onSpinWait()
since Java 9;Object.wait(long timeout)
andObject.wait(long timeout, int nanos)
;TimeUnit.sleep(long timeout)
;LockSupport.parkNanos(long nanos)
.
After reviewing the implementation of all methods in the available code of OpenJDK 9, this list can be simplified:
Object.wait(0, x)
isObject.wait(1)
;Thread.sleep(0, x)
isThread.sleep(0)
whenx
less than500,000
andThread.sleep(1)
otherwise;TimeUnit.sleep(long timeout)
is simple callingThread.sleep(long millis, int nanos)
.
Benchmarking
The benchmark was run on an Apple laptop:
Benchmark | ns/op | Error |
---|---|---|
LockSupport.parkNanos.(1) | 10553.701 | ± 309.104 |
Object.wait(0,1) | 1351918.387 | ± 7,285.491 |
Thread.onSpinWait() | 3.271 | ± 0.047 |
Thread.sleep(0) | 258.652 | ± 3.411 |
Thread.yield() | 225.066 | ± 3.808 |
and a server running Debian Sid:
Benchmark | ns/op | Error |
---|---|---|
LockSupport.parkNanos.(1) | 288.112 | ± 1.606 |
Object.wait(0,1) | 1120201.743 | ± 2,152.094 |
Thread.onSpinWait() | 2.496 | ± 0.003 |
Thread.sleep(0) | 183.340 | ± 2.365 |
Thread.yield() | 170.595 | ± 2.303 |
The anomalous behavior of LockSupport.parkNanos(1)
requires further
investigation with varying values.
Platform | Benchmark | ns/op | Error |
---|---|---|---|
macOS | LockSupport.parkNanos.(1) | 10553.701 | ± 309.104 |
Debian | LockSupport.parkNanos.(1) | 288.112 | ± 1.606 |
macOS | LockSupport.parkNanos.(10) | 13,006.281 | ± 73.393 |
Debian | LockSupport.parkNanos.(10) | 285.572 | ± 0.784 |
macOS | LockSupport.parkNanos.(100) | 12976.135 | ± 139.048 |
Debian | LockSupport.parkNanos.(100) | 4674.412 | ± 237.567 |
macOS | LockSupport.parkNanos.(1000) | 5795.405 | ± 132.921 |
Debian | LockSupport.parkNanos.(1000) | 55064.592 | ± 245.676 |
macOS | LockSupport.parkNanos.(10000) | 6127.251 | ± 203.732 |
Debian | LockSupport.parkNanos.(100000) | 54860.746 | ± 593.290 |
It is evident that LockSupport.parkNanos(1)
is highly unpredictable and
dependent on the platform used. The only guarantee is that a larger value will
result in a longer pause.
Benchmark Results
To summarize the benchmark results:
Thread.onSpinWait()
exists since Java 9 and takes only a few nanoseconds;Thread.yield()
is faster thanThread.sleep(0)
by approximately 13%, but it still takes a few hundred nanoseconds;Object.wait(0, 1)
results in approximately 1 millisecond of sleep;LockSupport.parkNanos(x)
has unpredictable behavior.
Based on these results, it can be concluded that the JVM cannot guarantee small pauses. There are two options available, but both of them depend on the machine and/or platform:
- either a small constant value such as
Thread.onSpinWait()
,Thread.yield()
, orThread.sleep(0)
; - or to suggest a pause, by
LockSupport.parkNanos(x)
, but keep in mind that its behavior cannot be guaranteed.
Multithreaded environment
I analyzed the benchmark in a clean environment with only one thread. However,
the benchmark can be run with a desired number of concurrent threads.
The log of the multithreaded benchmark was run on Debian on an
Intel(R) Xeon(R) CPU E5-1650 v3 @ 3.50GHz
.
Threads | Thread.onSpinWait() | Thread.yield() | Thread.sleep(0) |
---|---|---|---|
1 | 2.496981 | 170.287654 | 180.741691 |
2 | 2.512097 | 175.279333 | 191.246126 |
4 | 2.679693 | 206.098715 | 238.892798 |
8 | 3.160967 | 308.714533 | 332.272579 |
16 | 4.666055 | 694.989630 | 730.951726 |
32 | 10.782038 | 2733.348960 | 2815.557913 |
64 | 22.309719 | 7988.458300 | 7791.860280 |
128 | 38.417470 | 18454.350512 | 22313.944936 |
256 | 72.331578 | 35429.907284 | 37822.168572 |
This demonstrates that the pause time depends on the number of active threads:
Thread.onSpinWait()
causes a pause of 2.5 nanoseconds for 1 thread, 22.3 nanoseconds for 64 threads, and 72 nanoseconds for 256 threads;- on the other hand,
Thread.yield()
causes a pause of 170 nanoseconds for 1 thread, approximately 8 microseconds for 64 threads, and 35 microseconds for 256 threads; - and finally, the relation between
Thread.sleep(0)
andThread.yield()
holds the assumption.
Is it possible to predict? No. According to the documentation,
Thread.activeCount()
returns an estimated number of active threads, and the
returned value may change dynamically while the method traverses internal
structures. This means that it is seemingly impossible to compute in advance the
number of required Thread.onSpinWait()
or Thread.yield()
calls to achieve
the desired pause.
Conclusion
The JVM cannot guarantee sleep for very short periods of time.
The best available option is Thread.onSpinWait()
, which was introduced in
Java 9. It compiles into a CPU instruction similar to PAUSE
if the target CPU
supports it, and into nothing if it doesn’t. When it’s supported, the resulting
pause depends on the number of active threads, but it is on the
dozen-nanoseconds scale.
The second usable option is Thread.yield()
, which works like a hint to the
scheduler that this thread should be skipped. It may ignore or follow the
instruction. Doing so requires context switching, which can take microseconds in
real-life applications.