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)whenxless than500,000andThread.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.