Press "Enter" to skip to content

Backward-Compatible Thread#onSpinWait with MethodHandles

Thread#onSpinWait is an interesting JDK9 addition to the Thread API, which is used as a hint that we’re inside a spin-wait loop.

The method itself is… empty:

@HotSpotIntrinsicCandidate
public static void onSpinWait() {}

But it can be intrinsified to utilize, for example, x86’s PAUSE instruction – it’s a small thing but results in reduced power consumption and improved performance of spin-wait loops:

Improves the performance of spin-wait loops. When executing a “spin-wait loop,” a Pentium 4 or Intel Xeon processor suffers a severe performance penalty when exiting the loop because it detects a possible memory order violation. The PAUSE instruction provides a hint to the processor that the code sequence is a spin-wait loop. The processor uses this hint to avoid the memory order violation in most situations, which greatly improves processor performance. For this reason, it is recommended that a PAUSE instruction be placed in all spin-wait loops.

Unfortunately, it was introduced only in JDK9, and many libraries need to maintain JDK8(or even JDK6) compatibility.

However, since it’s just a void-returning method, what if we prepared a fallback for it when running on an earlier version of Java when it can’t really be used?

Thread#onSpinWait JDK8 Fallback

Java is a statically typed language, but if you’re missing these dynamic flavours, they are still accessible using, for example, Reflection or MethodHandles API, which allows you to invoke Java methods dynamically.

For example, a handle representing Thread#onSpinWait could be created using:

MethodHandles.lookup().findStatic(
  Thread.class, "onSpinWait", methodType(void.class))

So, we could do our best to resolve Thread#onSpinWait and when no luck, default to nothing, or some other method:

class OnSpinWaitJDK8Adapter {
    private static final MethodHandle ON_SPIN_WAIT_HANDLE = resolve();

    private OnSpinWaitJDK8Adapter() {
    }

    private static MethodHandle resolve() {
        try {
            return MethodHandles.lookup().findStatic(
              Thread.class, "onSpinWait", methodType(void.class));
        } catch (Exception ignore) {
        }

        return null;
    }

    static boolean onSpinWaitOrNothing() {
        if (ON_SPIN_WAIT_HANDLE != null) {
            try {
                ON_SPIN_WAIT_HANDLE.invokeExact();
                return true;
            } catch (Throwable ignore) {
            }
        }
        return false;
    }
}

Now we can spin-wait using our helper method instead which will resolve to NOOP on JDK8 and Thread#onSpinWait on JDK9+:

while (!flag.compareAndSet(false, true)) {
    OnSpinWaitJDK8Adapter.onSpinWaitOrNothing();
}

Or you could default to any other method that fits your use case:

while (!flag.compareAndSet(false, true)) {
    if (!OnSpinWaitJDK8Adapter.onSpinWaitOrNothing()) {
        // whatever else suits your needs
    }
}

And that’s all. There’s also no need to be worried about performance impact since the MethodHandle is resolved at startup time and invokeExact() of static methods are on-par with plain method calls, and null-checks will most-likely be elided, and methods inlined.

A simple benchmark proves that a resolved MethodHandle on a warmed-up JVM provides similar overhead to a plan static method call, and unresolved MethodHandle performs almost like an empty method:

Benchmark                     Mode  Cnt  Score   Error  Units
Bench.baseline_empty          avgt   15  0.388 ± 0.003  ns/op
Bench.baseline_mh_resolved    avgt   15  4.755 ± 0.039  ns/op
Bench.baseline_mh_unresolved  avgt   15  0.384 ± 0.001  ns/op
Bench.baseline_plain          avgt   15  4.739 ± 0.015  ns/op

Just out of pure curiosity, if we enable `-XX:+LogCompilation -XX:+PrintInlining`, we can additionally inspect how well the method gets inlined:

However, if extra overhead is still your concern, consider using Multi-Release JARs.

Sources

Agrona/Disruptor make use of a similar pattern: link.

The above code snippets can be found on GitHub.




If you enjoyed the content, consider supporting the site: