This demo runs {@code pinningCount} native-pinning calls alongside {@code unrelatedCount} + * ordinary virtual threads doing short, unrelated sleeps, on a carrier pool capped small enough + * that direct native calls would starve the unrelated work. {@code mode=direct} calls native code + * straight from the virtual thread (competes for the shared pool); {@code mode=bounded} routes it + * through a dedicated 2-thread executor instead. + * + *
Usage: {@code java -Djdk.virtualThreadScheduler.parallelism=2 BoundedNativeCallDemo + * direct|bounded} + */ +public final class BoundedNativeCallDemo { + + static { + System.loadLibrary("vtpinning"); + } + + private static native void blockingNativeCall(); + + @SuppressWarnings("unused") + private static void sleepCallback() { + try { + Thread.sleep(800); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public static void main(String[] args) throws Exception { + String mode = args.length > 0 ? args[0] : "direct"; + int pinningCount = 2; + int unrelatedCount = 6; + + ExecutorService nativePool = Executors.newFixedThreadPool(2); + try { + long start = System.nanoTime(); + + CountDownLatch pinningLatch = new CountDownLatch(pinningCount); + for (int i = 0; i < pinningCount; i++) { + Thread.ofVirtual().start(() -> { + try { + if ("bounded".equals(mode)) { + Future> f = nativePool.submit(BoundedNativeCallDemo::blockingNativeCall); + f.get(); + } else { + blockingNativeCall(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + pinningLatch.countDown(); + } + }); + } + + CountDownLatch unrelatedLatch = new CountDownLatch(unrelatedCount); + long[] unrelatedFinishedAt = new long[unrelatedCount]; + for (int i = 0; i < unrelatedCount; i++) { + int idx = i; + Thread.ofVirtual().start(() -> { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + unrelatedFinishedAt[idx] = (System.nanoTime() - start) / 1_000_000; + unrelatedLatch.countDown(); + } + }); + } + + unrelatedLatch.await(); + long unrelatedDoneMs = 0; + for (long t : unrelatedFinishedAt) { + unrelatedDoneMs = Math.max(unrelatedDoneMs, t); + } + pinningLatch.await(); + long totalMs = (System.nanoTime() - start) / 1_000_000; + + System.out.println("mode=" + mode + + " parallelism=" + System.getProperty("jdk.virtualThreadScheduler.parallelism") + + " pinningCount=" + pinningCount + + " unrelatedCount=" + unrelatedCount + + " unrelatedWorkDoneAtMs=" + unrelatedDoneMs + + " totalElapsedMs=" + totalMs); + } finally { + nativePool.shutdown(); + } + } +} diff --git a/vt-pinning/src/main/java/com/ankurm/vtpinning/MonitorPinningDemo.java b/vt-pinning/src/main/java/com/ankurm/vtpinning/MonitorPinningDemo.java new file mode 100644 index 0000000..b927490 --- /dev/null +++ b/vt-pinning/src/main/java/com/ankurm/vtpinning/MonitorPinningDemo.java @@ -0,0 +1,36 @@ +package com.ankurm.vtpinning; + +/** + * A virtual thread that enters a {@code synchronized} block and then blocks inside it. + * + *
Before JEP 491 (pre-JDK 24), this pinned the carrier thread for the full duration of the + * block. JEP 491 shipped GA in JDK 24 and removed that pinning: the virtual thread can now + * acquire, hold, and release a monitor independently of its carrier, so a {@code Thread.sleep} + * (or any other blocking call) inside {@code synchronized} no longer pins. + * + *
Run this with {@code -Djdk.tracePinnedThreads=full} on a pre-JDK-24 runtime and it prints a + * {@code reason:MONITOR} stack trace. Run it the same way on JDK 24+ and nothing prints at all - + * not because the flag broke, but because there is nothing left to report for this specific case. + * See {@link NativePinningDemo} for the pinning that JEP 491 did not touch. + */ +public final class MonitorPinningDemo { + + private static final Object LOCK = new Object(); + + public static void main(String[] args) throws Exception { + System.out.println("java.version=" + System.getProperty("java.version")); + System.out.println("jdk.tracePinnedThreads=" + System.getProperty("jdk.tracePinnedThreads")); + + Thread t = Thread.ofVirtual().start(() -> { + synchronized (LOCK) { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + t.join(); + System.out.println("done"); + } +} diff --git a/vt-pinning/src/main/java/com/ankurm/vtpinning/NativePinningDemo.java b/vt-pinning/src/main/java/com/ankurm/vtpinning/NativePinningDemo.java new file mode 100644 index 0000000..06fc70f --- /dev/null +++ b/vt-pinning/src/main/java/com/ankurm/vtpinning/NativePinningDemo.java @@ -0,0 +1,50 @@ +package com.ankurm.vtpinning; + +/** + * Reproduces the pinning JEP 491 did NOT fix: a virtual thread executing native code (a JNI + * native method, or the Foreign Function & Memory API) whose native frame calls back into + * Java code that blocks. The continuation backing the virtual thread cannot be frozen across a + * native stack frame, so the carrier stays pinned for as long as the callback blocks - + * regardless of JDK version, because this is architectural, not a bug JEP 491 targeted. + * + *
Requires {@code libvtpinning.so} on {@code java.library.path} (built by + * {@code scripts/build-native.sh} from {@code src/main/c/vtpinning.c}). The native method calls + * straight back into {@link #sleepCallback()}, which is the only thing that actually blocks. + * + *
Usage: {@code java --enable-native-access=ALL-UNNAMED NativePinningDemo [sleepMillis]} + * (sleepMillis defaults to 200; pass a larger value to hold the pin open long enough to attach a + * jcmd thread dump to it - see {@link ThreadDumpPinnedDemo}). + */ +public final class NativePinningDemo { + + static { + System.loadLibrary("vtpinning"); + } + + private static native void blockingNativeCall(); + + // Called back FROM native code, while native frames are still on this thread's stack. + @SuppressWarnings("unused") + private static void sleepCallback() { + try { + Thread.sleep(SLEEP_MILLIS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static volatile long SLEEP_MILLIS = 200; + + public static void main(String[] args) throws Exception { + if (args.length > 0) { + SLEEP_MILLIS = Long.parseLong(args[0]); + } + System.out.println("java.version=" + System.getProperty("java.version")); + System.out.println("jdk.tracePinnedThreads=" + System.getProperty("jdk.tracePinnedThreads")); + System.out.println("sleepMillis=" + SLEEP_MILLIS); + + Thread t = Thread.ofVirtual().start(NativePinningDemo::blockingNativeCall); + t.join(); + System.out.println("done"); + } +} diff --git a/vt-pinning/src/main/java/com/ankurm/vtpinning/PinningThroughputDemo.java b/vt-pinning/src/main/java/com/ankurm/vtpinning/PinningThroughputDemo.java new file mode 100644 index 0000000..164c9e5 --- /dev/null +++ b/vt-pinning/src/main/java/com/ankurm/vtpinning/PinningThroughputDemo.java @@ -0,0 +1,68 @@ +package com.ankurm.vtpinning; + +import java.util.concurrent.CountDownLatch; + +/** + * What native-code pinning actually costs in wall time, versus the same workload done in a way + * that unmounts normally. Runs {@code threadCount} virtual threads, each blocking for + * {@code blockMillis}, either through the native pinning path ({@code mode=native}) or through a + * plain {@code Thread.sleep} that unmounts ({@code mode=plain}). Run with + * {@code -Djdk.virtualThreadScheduler.parallelism=N} to cap the carrier pool and make the + * difference visible on a small thread count. + * + *
Usage: {@code java -Djdk.virtualThreadScheduler.parallelism=2 PinningThroughputDemo
+ * native|plain [threadCount] [blockMillis]}
+ */
+public final class PinningThroughputDemo {
+
+ static {
+ System.loadLibrary("vtpinning");
+ }
+
+ private static native void blockingNativeCall();
+
+ private static volatile long BLOCK_MILLIS = 1000;
+
+ @SuppressWarnings("unused")
+ private static void sleepCallback() {
+ try {
+ Thread.sleep(BLOCK_MILLIS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ String mode = args.length > 0 ? args[0] : "native";
+ int threadCount = args.length > 1 ? Integer.parseInt(args[1]) : 4;
+ if (args.length > 2) {
+ BLOCK_MILLIS = Long.parseLong(args[2]);
+ }
+
+ long start = System.nanoTime();
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ for (int i = 0; i < threadCount; i++) {
+ Thread.ofVirtual().start(() -> {
+ try {
+ if ("native".equals(mode)) {
+ blockingNativeCall();
+ } else {
+ Thread.sleep(BLOCK_MILLIS);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+ latch.await();
+ long elapsedMs = (System.nanoTime() - start) / 1_000_000;
+
+ System.out.println("mode=" + mode
+ + " threadCount=" + threadCount
+ + " blockMillis=" + BLOCK_MILLIS
+ + " parallelism=" + System.getProperty("jdk.virtualThreadScheduler.parallelism")
+ + " elapsedMs=" + elapsedMs);
+ }
+}
diff --git a/vt-pinning/src/main/java/com/ankurm/vtpinning/ThreadDumpPinnedDemo.java b/vt-pinning/src/main/java/com/ankurm/vtpinning/ThreadDumpPinnedDemo.java
new file mode 100644
index 0000000..c8624f0
--- /dev/null
+++ b/vt-pinning/src/main/java/com/ankurm/vtpinning/ThreadDumpPinnedDemo.java
@@ -0,0 +1,44 @@
+package com.ankurm.vtpinning;
+
+/**
+ * Companion to {@code scripts/run-all.sh}'s jcmd capture: starts one virtual thread pinned via
+ * native code (see {@link NativePinningDemo}) and one virtual thread merely sleeping (unpinned,
+ * unmounted), both long enough that a {@code jcmd