vt-pinning: diagnosing virtual thread pinning companion code
Reproduces the pinning JEP 491 did not fix (native JNI frames) with a real native library, alongside the monitor case it did fix. Covers the jdk.VirtualThreadPinned JFR event, reading pinned vs unmounted virtual threads from a jcmd Thread.dump_to_file, the measured throughput cost on a capped carrier pool, and a bounded-executor pattern to contain the blast radius. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FhzLY5p6okFva3qsnsRyvM
This commit is contained in:
@@ -8,6 +8,7 @@ article; each module's own README has that article's version table, quickstart,
|
|||||||
| [`jmm`](jmm/) | The Java Memory Model Explained: volatile, happens-before, and Why Your Double-Checked Lock Failed |
|
| [`jmm`](jmm/) | The Java Memory Model Explained: volatile, happens-before, and Why Your Double-Checked Lock Failed |
|
||||||
| [`locks`](locks/) | synchronized vs ReentrantLock vs StampedLock: Benchmarks and a Decision Table |
|
| [`locks`](locks/) | synchronized vs ReentrantLock vs StampedLock: Benchmarks and a Decision Table |
|
||||||
| [`atomics`](atomics/) | Java Atomics and VarHandle: CAS, LongAdder, and When Atomics Beat Locks |
|
| [`atomics`](atomics/) | Java Atomics and VarHandle: CAS, LongAdder, and When Atomics Beat Locks |
|
||||||
|
| [`vt-pinning`](vt-pinning/) | Diagnosing Virtual Thread Pinning in Production: JFR Events, jcmd, and Real Fixes |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<module>jmm</module>
|
<module>jmm</module>
|
||||||
<module>locks</module>
|
<module>locks</module>
|
||||||
<module>atomics</module>
|
<module>atomics</module>
|
||||||
|
<module>vt-pinning</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# vt-pinning
|
||||||
|
|
||||||
|
Companion code for the ankurm.com post *"Diagnosing Virtual Thread Pinning in Production: JFR
|
||||||
|
Events, jcmd, and Real Fixes."* Fourth module in `java-core-examples`, the Java-core /
|
||||||
|
concurrency series.
|
||||||
|
|
||||||
|
[JEP 491](https://openjdk.org/jeps/491) (GA in JDK 24) fixed the pinning caused by `synchronized`.
|
||||||
|
It did not touch the pinning caused by native code - a virtual thread executing a JNI native
|
||||||
|
method (or an FFM downcall) whose native frame calls back into blocking Java code still pins its
|
||||||
|
carrier, on every JDK version, because a continuation cannot be frozen across a native stack
|
||||||
|
frame. This module reproduces that with a real, tiny JNI library (not asserted from documentation)
|
||||||
|
and works through the diagnostics that still catch it once the old `-Djdk.tracePinnedThreads` flag
|
||||||
|
stops reporting anything at all.
|
||||||
|
|
||||||
|
## Versions this was built and tested against
|
||||||
|
|
||||||
|
| Component | Version | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| JDK (primary) | 25.0.4.1+1 (Temurin, LTS) | Has JEP 491; native pinning still reproduces. |
|
||||||
|
| JDK (comparison) | 21.0.10 (OpenJDK) | Pre-JEP-491, for the before/after. |
|
||||||
|
| gcc | system default | Builds the JNI native library; not committed, built by `scripts/build-native.sh`. |
|
||||||
|
| Maven | 3.9.11 | Compiles the Java classes only - no JMH in this module. |
|
||||||
|
| Hardware | 2 vCPU x86-64 VM | Same sandbox as the rest of this series. |
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export JDK25_HOME=/path/to/jdk-25
|
||||||
|
export JDK21_HOME=/path/to/jdk-21
|
||||||
|
export JAVA_HOME=$JDK21_HOME # any JDK's jni.h works for the native build
|
||||||
|
./scripts/build-native.sh
|
||||||
|
$JDK25_HOME/bin/javac --release 25 -d target/classes $(find src/main/java -name "*.java")
|
||||||
|
$JDK25_HOME/bin/java --enable-native-access=ALL-UNNAMED -Djava.library.path=target/native \
|
||||||
|
-cp target/classes com.ankurm.vtpinning.NativePinningDemo
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/run-all.sh` regenerates every file in `output/` (needs both `JDK25_HOME` and
|
||||||
|
`JDK21_HOME` set, plus `gcc` on `PATH`).
|
||||||
|
|
||||||
|
## What's in here
|
||||||
|
|
||||||
|
| File | What it shows |
|
||||||
|
|---|---|
|
||||||
|
| `src/main/c/vtpinning.c` | One JNI native function, shared by every demo class: calls back into a static Java method on the same native stack frame, so the callback's blocking happens with native frames still present. |
|
||||||
|
| `.../MonitorPinningDemo.java` | `synchronized` + `Thread.sleep` on a virtual thread - the case JEP 491 fixed. |
|
||||||
|
| `.../NativePinningDemo.java` | The case JEP 491 did not fix: a JNI native method whose callback blocks. |
|
||||||
|
| `.../ThreadDumpPinnedDemo.java` | One pinned and one unpinned virtual thread running concurrently, for a `jcmd Thread.dump_to_file` capture. |
|
||||||
|
| `.../PinningThroughputDemo.java` | The wall-time cost of native pinning vs. plain blocking, on a capped carrier pool. |
|
||||||
|
| `.../BoundedNativeCallDemo.java` | The fix: routing a pinning native call through a small dedicated executor instead of calling it directly from the shared carrier pool. |
|
||||||
|
| `output/01` | `MonitorPinningDemo` on JDK 21 vs JDK 25, with `-Djdk.tracePinnedThreads=full`. |
|
||||||
|
| `output/02` | `NativePinningDemo` on JDK 21 vs JDK 25, same flag - still fires on 21, silent on 25. |
|
||||||
|
| `output/03` | The `jdk.VirtualThreadPinned` JFR event, captured and printed with `jfr print`, for the same native pin on JDK 25. |
|
||||||
|
| `output/04` | A `jcmd Thread.dump_to_file -format=json` snapshot taken mid-pin, pinned and unpinned virtual thread entries side by side. |
|
||||||
|
| `output/05` | `PinningThroughputDemo`: 4 virtual threads / 2 carriers, native vs plain. |
|
||||||
|
| `output/06` | `BoundedNativeCallDemo`: direct vs bounded-executor routing, effect on unrelated work. |
|
||||||
|
|
||||||
|
## Reading the numbers honestly (2-vCPU sandbox)
|
||||||
|
|
||||||
|
**The old diagnostic flag doesn't just go quiet for the case JEP 491 fixed - it's silent for the
|
||||||
|
case JEP 491 left alone too** (`output/02`). `-Djdk.tracePinnedThreads=full` reports
|
||||||
|
`reason:NATIVE` on JDK 21 for the exact same native call that still measurably pins on JDK 25
|
||||||
|
(confirmed independently via the JFR event in `output/03`, and via the throughput cost in
|
||||||
|
`output/05`). A team that kept the flag in their JVM args through an upgrade to JDK 24+ gets no
|
||||||
|
signal from it for either kind of pinning going forward - not just the kind that stopped
|
||||||
|
mattering.
|
||||||
|
|
||||||
|
**A `jcmd` thread dump distinguishes pinned from unmounted with one field** (`output/04`): a
|
||||||
|
pinned virtual thread's JSON entry carries a `"carrier"` field naming the platform thread's `tid`,
|
||||||
|
and its stack includes `VirtualThread.parkOnCarrierThread`. An unmounted (not pinned) blocked
|
||||||
|
virtual thread has neither - no `carrier` field at all, and a plain `parkNanos` in its stack. This
|
||||||
|
needs no JFR recording running and works against a single dump taken after the fact.
|
||||||
|
|
||||||
|
**Native pinning genuinely serializes work on a capped carrier pool** (`output/05`): 4 virtual
|
||||||
|
threads each blocking ~1000ms on 2 carriers finish in ~2015ms through the native path (two
|
||||||
|
carrier-bound batches) versus ~1022ms through plain `Thread.sleep` (all four unmount and share the
|
||||||
|
2 carriers freely). This isn't a benchmark artifact - it's the same mechanism JEP 491 fixed for
|
||||||
|
monitors, just for a case it didn't touch.
|
||||||
|
|
||||||
|
**Routing the pinning call through a small dedicated executor protects everything else**
|
||||||
|
(`output/06`): with 2 carriers, 2 concurrent native-pinning calls, and 6 unrelated
|
||||||
|
100ms-sleep virtual threads competing for the same pool, calling the native code directly starves
|
||||||
|
the unrelated work until ~924ms - both carriers are pinned the whole time. Routing the same native
|
||||||
|
calls through a 2-thread dedicated executor (via `Future.get()`, which parks normally with no
|
||||||
|
native frame on *that* thread's own stack) lets the unrelated work finish in ~126ms while the
|
||||||
|
native calls run to completion in the background. The pinning cost doesn't go away, but it stops
|
||||||
|
being everyone else's problem.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT - see the [repo-wide LICENSE](../LICENSE).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
$ java -Djdk.tracePinnedThreads=full MonitorPinningDemo (pre-JDK-24 runtime)
|
||||||
|
|
||||||
|
java.version=21.0.10
|
||||||
|
jdk.tracePinnedThreads=full
|
||||||
|
VirtualThread[#18]/runnable@ForkJoinPool-1-worker-1 reason:MONITOR
|
||||||
|
java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:199)
|
||||||
|
java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
|
||||||
|
java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:635)
|
||||||
|
java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:807)
|
||||||
|
java.base/java.lang.Thread.sleep(Thread.java:507)
|
||||||
|
com.ankurm.vtpinning.MonitorPinningDemo.lambda$main$0(MonitorPinningDemo.java:27) <== monitors:1
|
||||||
|
java.base/java.lang.VirtualThread.run(VirtualThread.java:329)
|
||||||
|
done
|
||||||
|
|
||||||
|
$ java -Djdk.tracePinnedThreads=full MonitorPinningDemo (JDK 24+ runtime)
|
||||||
|
|
||||||
|
java.version=25.0.4.1
|
||||||
|
jdk.tracePinnedThreads=full
|
||||||
|
done
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
$ java -Djdk.tracePinnedThreads=full NativePinningDemo (pre-JDK-24 runtime)
|
||||||
|
|
||||||
|
java.version=21.0.10
|
||||||
|
jdk.tracePinnedThreads=full
|
||||||
|
sleepMillis=200
|
||||||
|
VirtualThread[#18]/runnable@ForkJoinPool-1-worker-1 reason:NATIVE
|
||||||
|
java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:199)
|
||||||
|
java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
|
||||||
|
java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:635)
|
||||||
|
java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:807)
|
||||||
|
java.base/java.lang.Thread.sleep(Thread.java:507)
|
||||||
|
com.ankurm.vtpinning.NativePinningDemo.sleepCallback(NativePinningDemo.java:30)
|
||||||
|
com.ankurm.vtpinning.NativePinningDemo.blockingNativeCall(Native Method)
|
||||||
|
java.base/java.lang.VirtualThread.run(VirtualThread.java:329)
|
||||||
|
done
|
||||||
|
|
||||||
|
$ java -Djdk.tracePinnedThreads=full NativePinningDemo (JDK 24+ runtime - flag is inert)
|
||||||
|
|
||||||
|
java.version=25.0.4.1
|
||||||
|
jdk.tracePinnedThreads=full
|
||||||
|
sleepMillis=200
|
||||||
|
done
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
$ java -XX:StartFlightRecording=...,jdk.VirtualThreadPinned#enabled=true,threshold=0ms NativePinningDemo
|
||||||
|
|
||||||
|
java.version=25.0.4.1
|
||||||
|
jdk.tracePinnedThreads=null
|
||||||
|
sleepMillis=200
|
||||||
|
done
|
||||||
|
|
||||||
|
$ jfr print --events jdk.VirtualThreadPinned --stack-depth 10 target/native-pin.jfr
|
||||||
|
|
||||||
|
jdk.VirtualThreadPinned {
|
||||||
|
startTime = 12:02:49.269 (2026-09-30)
|
||||||
|
duration = 200 ms
|
||||||
|
blockingOperation = "LockSupport.park"
|
||||||
|
pinnedReason = "Native or VM frame on stack"
|
||||||
|
carrierThread = "ForkJoinPool-1-worker-1" (javaThreadId = 28)
|
||||||
|
eventThread = "" (javaThreadId = 27, virtual)
|
||||||
|
stackTrace = [
|
||||||
|
java.lang.VirtualThread.parkOnCarrierThread(boolean, long) line: 833
|
||||||
|
java.lang.VirtualThread.parkNanos(long) line: 801
|
||||||
|
java.lang.VirtualThread.sleepNanos(long) line: 983
|
||||||
|
java.lang.Thread.sleepNanos(long) line: 507
|
||||||
|
java.lang.Thread.sleep(long) line: 540
|
||||||
|
com.ankurm.vtpinning.NativePinningDemo.sleepCallback() line: 30
|
||||||
|
com.ankurm.vtpinning.NativePinningDemo.blockingNativeCall()
|
||||||
|
java.lang.VirtualThread.run(Runnable) line: 460
|
||||||
|
jdk.internal.vm.Continuation.enterSpecial(Continuation, boolean, boolean)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
$ java ThreadDumpPinnedDemo &
|
||||||
|
pid=7126
|
||||||
|
done
|
||||||
|
|
||||||
|
$ jcmd <pid> Thread.dump_to_file -format=json threaddump.json (taken mid-flight)
|
||||||
|
7126:
|
||||||
|
Created /home/claude/java-core-examples/vt-pinning/target/threaddump.json
|
||||||
|
|
||||||
|
--- the two virtual thread entries from that dump ---
|
||||||
|
{
|
||||||
|
"tid": "22",
|
||||||
|
"time": "2026-09-30T06:32:51.489994627Z",
|
||||||
|
"virtual": true,
|
||||||
|
"name": "",
|
||||||
|
"state": "TIMED_WAITING",
|
||||||
|
"stack": [
|
||||||
|
"java.base/jdk.internal.misc.Unsafe.park(Native Method)",
|
||||||
|
"java.base/java.lang.VirtualThread.parkOnCarrierThread(VirtualThread.java:822)",
|
||||||
|
"java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:801)",
|
||||||
|
"java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:983)",
|
||||||
|
"java.base/java.lang.Thread.sleepNanos(Thread.java:507)",
|
||||||
|
"java.base/java.lang.Thread.sleep(Thread.java:540)",
|
||||||
|
"com.ankurm.vtpinning.ThreadDumpPinnedDemo.sleepCallback(ThreadDumpPinnedDemo.java:22)",
|
||||||
|
"com.ankurm.vtpinning.ThreadDumpPinnedDemo.blockingNativeCall(Native Method)",
|
||||||
|
"java.base/java.lang.VirtualThread.run(VirtualThread.java:460)"
|
||||||
|
],
|
||||||
|
"carrier": "23"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"tid": "24",
|
||||||
|
"time": "2026-09-30T06:32:51.490322496Z",
|
||||||
|
"virtual": true,
|
||||||
|
"name": "",
|
||||||
|
"state": "TIMED_WAITING",
|
||||||
|
"stack": [
|
||||||
|
"java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:787)",
|
||||||
|
"java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:983)",
|
||||||
|
"java.base/java.lang.Thread.sleepNanos(Thread.java:507)",
|
||||||
|
"java.base/java.lang.Thread.sleep(Thread.java:540)",
|
||||||
|
"com.ankurm.vtpinning.ThreadDumpPinnedDemo.lambda$main$0(ThreadDumpPinnedDemo.java:34)",
|
||||||
|
"java.base/java.lang.VirtualThread.run(VirtualThread.java:460)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
$ java -Djdk.virtualThreadScheduler.parallelism=2 PinningThroughputDemo native 4 1000
|
||||||
|
mode=native threadCount=4 blockMillis=1000 parallelism=2 elapsedMs=2015
|
||||||
|
|
||||||
|
$ java -Djdk.virtualThreadScheduler.parallelism=2 PinningThroughputDemo plain 4 1000
|
||||||
|
|
||||||
|
mode=plain threadCount=4 blockMillis=1000 parallelism=2 elapsedMs=1022
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
$ java -Djdk.virtualThreadScheduler.parallelism=2 BoundedNativeCallDemo direct
|
||||||
|
mode=direct parallelism=2 pinningCount=2 unrelatedCount=6 unrelatedWorkDoneAtMs=924 totalElapsedMs=924
|
||||||
|
|
||||||
|
$ java -Djdk.virtualThreadScheduler.parallelism=2 BoundedNativeCallDemo bounded
|
||||||
|
mode=bounded parallelism=2 pinningCount=2 unrelatedCount=6 unrelatedWorkDoneAtMs=126 totalElapsedMs=826
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>java-core-examples</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>vt-pinning</artifactId>
|
||||||
|
<name>vt-pinning</name>
|
||||||
|
<description>Diagnosing virtual thread pinning in production: reproducing the native-code pinning JEP 491 didn't fix, reading the jdk.VirtualThreadPinned JFR event, and finding a pinned carrier in a jcmd thread dump.</description>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<release>25</release>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Compiles src/main/c/vtpinning.c into libvtpinning.so, linked against the JNI headers of
|
||||||
|
# whichever JDK is on JAVA_HOME (any JDK's jni.h works - the JNI ABI is stable across versions;
|
||||||
|
# this repo built it once against JDK 21's headers and loads it fine under JDK 21 and JDK 25).
|
||||||
|
# The .so is NOT committed to the repo; run this before the other scripts.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
: "${JAVA_HOME:?Set JAVA_HOME to a JDK install (its include/ dir must have jni.h)}"
|
||||||
|
|
||||||
|
mkdir -p target/native
|
||||||
|
gcc -shared -fPIC \
|
||||||
|
-I"$JAVA_HOME/include" \
|
||||||
|
-I"$JAVA_HOME/include/linux" \
|
||||||
|
-o target/native/libvtpinning.so \
|
||||||
|
src/main/c/vtpinning.c
|
||||||
|
|
||||||
|
echo "Built target/native/libvtpinning.so"
|
||||||
Executable
+112
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates every file in output/. Requires:
|
||||||
|
# - JDK25_HOME pointing at a JDK 24+ install (this repo used Temurin 25.0.4.1+1)
|
||||||
|
# - JDK21_HOME pointing at a pre-JDK-24 install (this repo used OpenJDK 21.0.10), for the
|
||||||
|
# before/after comparisons against JEP 491
|
||||||
|
# - gcc on PATH (builds the JNI native library)
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
: "${JDK25_HOME:?Set JDK25_HOME to a JDK 24+ install}"
|
||||||
|
: "${JDK21_HOME:?Set JDK21_HOME to a pre-JDK-24 install}"
|
||||||
|
|
||||||
|
JAVA_HOME="$JDK21_HOME" ./scripts/build-native.sh
|
||||||
|
|
||||||
|
mkdir -p target/classes target/classes-21
|
||||||
|
"$JDK25_HOME/bin/javac" --release 25 -d target/classes $(find src/main/java -name "*.java")
|
||||||
|
"$JDK21_HOME/bin/javac" --release 21 -d target/classes-21 $(find src/main/java -name "*.java")
|
||||||
|
|
||||||
|
NATLIB=target/native
|
||||||
|
|
||||||
|
echo "--- 01: monitor pinning, JDK21 vs JDK25 (JEP 491) ---"
|
||||||
|
{
|
||||||
|
echo '$ java -Djdk.tracePinnedThreads=full MonitorPinningDemo (pre-JDK-24 runtime)'
|
||||||
|
echo ""
|
||||||
|
"$JDK21_HOME/bin/java" -Djdk.tracePinnedThreads=full -cp target/classes-21 \
|
||||||
|
com.ankurm.vtpinning.MonitorPinningDemo 2>&1 | grep -v "Picked up"
|
||||||
|
echo ""
|
||||||
|
echo '$ java -Djdk.tracePinnedThreads=full MonitorPinningDemo (JDK 24+ runtime)'
|
||||||
|
echo ""
|
||||||
|
"$JDK25_HOME/bin/java" -Djdk.tracePinnedThreads=full -cp target/classes \
|
||||||
|
com.ankurm.vtpinning.MonitorPinningDemo 2>&1 | grep -v "Picked up"
|
||||||
|
} > output/01-monitor-pinning-jdk21-vs-jdk25.txt
|
||||||
|
|
||||||
|
echo "--- 02: native pinning, JDK21 vs JDK25 (still real on both) ---"
|
||||||
|
{
|
||||||
|
echo '$ java -Djdk.tracePinnedThreads=full NativePinningDemo (pre-JDK-24 runtime)'
|
||||||
|
echo ""
|
||||||
|
"$JDK21_HOME/bin/java" -Djdk.tracePinnedThreads=full -Djava.library.path=$NATLIB -cp target/classes-21 \
|
||||||
|
com.ankurm.vtpinning.NativePinningDemo 2>&1 | grep -v "Picked up"
|
||||||
|
echo ""
|
||||||
|
echo '$ java -Djdk.tracePinnedThreads=full NativePinningDemo (JDK 24+ runtime - flag is inert)'
|
||||||
|
echo ""
|
||||||
|
"$JDK25_HOME/bin/java" --enable-native-access=ALL-UNNAMED -Djdk.tracePinnedThreads=full -Djava.library.path=$NATLIB -cp target/classes \
|
||||||
|
com.ankurm.vtpinning.NativePinningDemo 2>&1 | grep -v "Picked up\|WARNING"
|
||||||
|
} > output/02-native-pinning-jdk21-vs-jdk25.txt
|
||||||
|
|
||||||
|
echo "--- 03: jdk.VirtualThreadPinned JFR event on JDK25 ---"
|
||||||
|
rm -f target/native-pin.jfr
|
||||||
|
{
|
||||||
|
echo '$ java -XX:StartFlightRecording=...,jdk.VirtualThreadPinned#enabled=true,threshold=0ms NativePinningDemo'
|
||||||
|
echo ""
|
||||||
|
"$JDK25_HOME/bin/java" --enable-native-access=ALL-UNNAMED \
|
||||||
|
"-XX:StartFlightRecording=filename=target/native-pin.jfr,settings=profile,jdk.VirtualThreadPinned#enabled=true,jdk.VirtualThreadPinned#threshold=0ms,jdk.VirtualThreadPinned#stackTrace=true" \
|
||||||
|
-Djava.library.path=$NATLIB -cp target/classes com.ankurm.vtpinning.NativePinningDemo 2>&1 \
|
||||||
|
| grep -v "Picked up\|WARNING\|jfr,startup\|^\["
|
||||||
|
echo ""
|
||||||
|
echo '$ jfr print --events jdk.VirtualThreadPinned --stack-depth 10 target/native-pin.jfr'
|
||||||
|
echo ""
|
||||||
|
"$JDK25_HOME/bin/jfr" print --events jdk.VirtualThreadPinned --stack-depth 10 target/native-pin.jfr 2>&1 | grep -v "Picked up"
|
||||||
|
} > output/03-jfr-virtualthreadpinned-event.txt
|
||||||
|
|
||||||
|
echo "--- 04: jcmd Thread.dump_to_file, pinned vs unpinned virtual thread ---"
|
||||||
|
rm -f target/threaddump.json target/threaddump_out.txt
|
||||||
|
"$JDK25_HOME/bin/java" --enable-native-access=ALL-UNNAMED -Djava.library.path=$NATLIB -cp target/classes \
|
||||||
|
com.ankurm.vtpinning.ThreadDumpPinnedDemo > target/threaddump_out.txt 2>&1 &
|
||||||
|
PID=$!
|
||||||
|
sleep 1.5
|
||||||
|
"$JDK25_HOME/bin/jcmd" "$PID" Thread.dump_to_file -format=json target/threaddump.json > target/jcmd_out.txt 2>&1
|
||||||
|
wait $PID
|
||||||
|
{
|
||||||
|
echo '$ java ThreadDumpPinnedDemo &'
|
||||||
|
grep -v "Picked up" target/threaddump_out.txt
|
||||||
|
echo ""
|
||||||
|
echo '$ jcmd <pid> Thread.dump_to_file -format=json threaddump.json (taken mid-flight)'
|
||||||
|
grep -v "Picked up" target/jcmd_out.txt
|
||||||
|
echo ""
|
||||||
|
echo '--- the two virtual thread entries from that dump ---'
|
||||||
|
python3 - <<'PYEOF'
|
||||||
|
import json
|
||||||
|
with open('target/threaddump.json') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
for c in data['threadDump']['threadContainers']:
|
||||||
|
for t in c.get('threads', []):
|
||||||
|
if t.get('virtual'):
|
||||||
|
print(json.dumps(t, indent=2))
|
||||||
|
print()
|
||||||
|
PYEOF
|
||||||
|
} > output/04-jcmd-thread-dump-pinned-vs-unpinned.txt
|
||||||
|
|
||||||
|
echo "--- 05: throughput cost of native pinning vs plain blocking (capped carriers) ---"
|
||||||
|
{
|
||||||
|
echo '$ java -Djdk.virtualThreadScheduler.parallelism=2 PinningThroughputDemo native 4 1000'
|
||||||
|
"$JDK25_HOME/bin/java" --enable-native-access=ALL-UNNAMED -Djdk.virtualThreadScheduler.parallelism=2 -Djava.library.path=$NATLIB -cp target/classes \
|
||||||
|
com.ankurm.vtpinning.PinningThroughputDemo native 4 1000 2>&1 | grep -v "Picked up\|WARNING"
|
||||||
|
echo ""
|
||||||
|
echo '$ java -Djdk.virtualThreadScheduler.parallelism=2 PinningThroughputDemo plain 4 1000'
|
||||||
|
"$JDK25_HOME/bin/java" -Djdk.virtualThreadScheduler.parallelism=2 -Djava.library.path=$NATLIB -cp target/classes \
|
||||||
|
com.ankurm.vtpinning.PinningThroughputDemo plain 4 1000 2>&1 | grep -v "Picked up\|WARNING"
|
||||||
|
} > output/05-pinning-throughput-cost.txt
|
||||||
|
|
||||||
|
echo "--- 06: bounding the blast radius with a dedicated executor ---"
|
||||||
|
{
|
||||||
|
echo '$ java -Djdk.virtualThreadScheduler.parallelism=2 BoundedNativeCallDemo direct'
|
||||||
|
"$JDK25_HOME/bin/java" --enable-native-access=ALL-UNNAMED -Djdk.virtualThreadScheduler.parallelism=2 -Djava.library.path=$NATLIB -cp target/classes \
|
||||||
|
com.ankurm.vtpinning.BoundedNativeCallDemo direct 2>&1 | grep -v "Picked up\|WARNING"
|
||||||
|
echo ""
|
||||||
|
echo '$ java -Djdk.virtualThreadScheduler.parallelism=2 BoundedNativeCallDemo bounded'
|
||||||
|
"$JDK25_HOME/bin/java" --enable-native-access=ALL-UNNAMED -Djdk.virtualThreadScheduler.parallelism=2 -Djava.library.path=$NATLIB -cp target/classes \
|
||||||
|
com.ankurm.vtpinning.BoundedNativeCallDemo bounded 2>&1 | grep -v "Picked up\|WARNING"
|
||||||
|
} > output/06-bounded-executor-fix.txt
|
||||||
|
|
||||||
|
echo "Done. See output/."
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* A minimal JNI native method used by every demo in this module. It does exactly one thing:
|
||||||
|
* call back into the Java class that invoked it, on the same native stack frame, so that
|
||||||
|
* whatever the callback does (in every demo here, a Thread.sleep) executes while native frames
|
||||||
|
* are still on the virtual thread's stack - which is precisely what a continuation cannot freeze
|
||||||
|
* across, and therefore what still pins a virtual thread's carrier on every JDK version,
|
||||||
|
* including JDK 24+ after JEP 491 fixed monitor pinning.
|
||||||
|
*
|
||||||
|
* Every demo class in this module declares the same native method name
|
||||||
|
* (blockingNativeCall) and static callback name (sleepCallback), just in different classes, so
|
||||||
|
* one shared native function - looked up by class name at call time - covers all of them.
|
||||||
|
*/
|
||||||
|
#include <jni.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* JNI_OnLoad isn't required here; each demo class binds via System.loadLibrary + a matching
|
||||||
|
* Java_<class>_blockingNativeCall symbol would normally be needed per class. Instead we export
|
||||||
|
* one symbol per demo class below, all doing the identical call-back, to avoid duplicating this
|
||||||
|
* file four times for four otherwise-identical native methods. */
|
||||||
|
|
||||||
|
static void callSleepCallback(JNIEnv *env, jclass cls) {
|
||||||
|
jmethodID mid = (*env)->GetStaticMethodID(env, cls, "sleepCallback", "()V");
|
||||||
|
if (mid == NULL) {
|
||||||
|
return; /* let the pending exception propagate back into Java */
|
||||||
|
}
|
||||||
|
(*env)->CallStaticVoidMethod(env, cls, mid);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_com_ankurm_vtpinning_NativePinningDemo_blockingNativeCall
|
||||||
|
(JNIEnv *env, jclass cls) {
|
||||||
|
callSleepCallback(env, cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_com_ankurm_vtpinning_ThreadDumpPinnedDemo_blockingNativeCall
|
||||||
|
(JNIEnv *env, jclass cls) {
|
||||||
|
callSleepCallback(env, cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_com_ankurm_vtpinning_PinningThroughputDemo_blockingNativeCall
|
||||||
|
(JNIEnv *env, jclass cls) {
|
||||||
|
callSleepCallback(env, cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_com_ankurm_vtpinning_BoundedNativeCallDemo_blockingNativeCall
|
||||||
|
(JNIEnv *env, jclass cls) {
|
||||||
|
callSleepCallback(env, cls);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.ankurm.vtpinning;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The fix for native-code pinning isn't eliminating it - it's architectural, and no JDK flag
|
||||||
|
* removes it. The fix is containing its blast radius: never let a native call that pins run
|
||||||
|
* directly on the same carrier pool everything else in the process shares. Route it through a
|
||||||
|
* small, explicitly-sized platform-thread executor instead, and call that executor from the
|
||||||
|
* virtual thread via a plain {@code Future.get()} - which parks normally (no native frame on
|
||||||
|
* *this* thread's stack) and unmounts, leaving the shared carrier pool free for unrelated work.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ankurm.vtpinning;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A virtual thread that enters a {@code synchronized} block and then blocks inside it.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <pid> Thread.dump_to_file -format=json} taken
|
||||||
|
* a second later catches both mid-flight. Compare the two entries in the dump: the pinned one
|
||||||
|
* carries a {@code "carrier"} field naming the platform thread it is stuck to and its stack shows
|
||||||
|
* {@code VirtualThread.parkOnCarrierThread}; the unpinned one has neither.
|
||||||
|
*/
|
||||||
|
public final class ThreadDumpPinnedDemo {
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.loadLibrary("vtpinning");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native void blockingNativeCall();
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private static void sleepCallback() {
|
||||||
|
try {
|
||||||
|
Thread.sleep(4000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
System.out.println("pid=" + ProcessHandle.current().pid());
|
||||||
|
|
||||||
|
Thread pinned = Thread.ofVirtual().start(ThreadDumpPinnedDemo::blockingNativeCall);
|
||||||
|
Thread unpinned = Thread.ofVirtual().start(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(4000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pinned.join();
|
||||||
|
unpinned.join();
|
||||||
|
System.out.println("done");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user