Skip to content

Instantly share code, notes, and snippets.

@HomoEfficio
Last active January 5, 2026 04:16
Show Gist options
  • Select an option

  • Save HomoEfficio/31c75c0774851c294c87e324f4be466c to your computer and use it in GitHub Desktop.

Select an option

Save HomoEfficio/31c75c0774851c294c87e324f4be466c to your computer and use it in GitHub Desktop.
In Java 21, a virtual thread stays mounted on a carrier thread while waiting to acquire a lock of the synchronized block.
package ca.bazlur.modern.concurrency.c02;
import java.util.List;
import java.util.stream.IntStream;
public class ThreadPinningSynchronizedTest {
private static final Object lock = new Object();
public static void main(String[] args) {
// Give some time for JFR ready
giveSomeTimeForJFRInit();
List<Thread> threads = IntStream.range(0, 10)
.mapToObj(index -> Thread.ofVirtual().unstarted(() -> {
System.out.printf("BEFORE %d - %s%n", index, Thread.currentThread());
// HERE!
// A virtual thread stays mounted on a carrier while waiting for the lock.
// But not because the virtual thread is pinned to the carrier,
// but because Java 21 `synchronized` does not know how to deal with virtual threads.
synchronized (lock) {
// No blocking but takes long time
getFibonacci(40);
}
System.out.printf("AFTER %d - %s%n", index, Thread.currentThread());
}))
.toList();
threads.forEach(Thread::start);
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// n-th Fibonacci number
private static long getFibonacci(int n) {
if (n <= 1) {
return n;
}
return getFibonacci(n - 1) + getFibonacci(n - 2);
}
private static void giveSomeTimeForJFRInit() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
/*
The result using JDK 21
BEFORE 0 - VirtualThread[#31]/runnable@ForkJoinPool-1-worker-1
BEFORE 1 - VirtualThread[#32]/runnable@ForkJoinPool-1-worker-3
BEFORE 2 - VirtualThread[#33]/runnable@ForkJoinPool-1-worker-4
BEFORE 3 - VirtualThread[#34]/runnable@ForkJoinPool-1-worker-6
BEFORE 4 - VirtualThread[#35]/runnable@ForkJoinPool-1-worker-5
BEFORE 6 - VirtualThread[#37]/runnable@ForkJoinPool-1-worker-2
BEFORE 5 - VirtualThread[#36]/runnable@ForkJoinPool-1-worker-7
BEFORE 8 - VirtualThread[#39]/runnable@ForkJoinPool-1-worker-9
BEFORE 9 - VirtualThread[#40]/runnable@ForkJoinPool-1-worker-8
BEFORE 7 - VirtualThread[#38]/runnable@ForkJoinPool-1-worker-10
AFTER 0 - VirtualThread[#31]/runnable@ForkJoinPool-1-worker-1
AFTER 7 - VirtualThread[#38]/runnable@ForkJoinPool-1-worker-10
AFTER 9 - VirtualThread[#40]/runnable@ForkJoinPool-1-worker-8
AFTER 8 - VirtualThread[#39]/runnable@ForkJoinPool-1-worker-9
AFTER 5 - VirtualThread[#36]/runnable@ForkJoinPool-1-worker-7
AFTER 6 - VirtualThread[#37]/runnable@ForkJoinPool-1-worker-2
AFTER 4 - VirtualThread[#35]/runnable@ForkJoinPool-1-worker-5
AFTER 3 - VirtualThread[#34]/runnable@ForkJoinPool-1-worker-6
AFTER 2 - VirtualThread[#33]/runnable@ForkJoinPool-1-worker-4
AFTER 1 - VirtualThread[#32]/runnable@ForkJoinPool-1-worker-3
가상 스레드별 BEFORE/AFTER 가 동일하다.
즉, 가상 스레드가 캐리어 스레드를 점유하고 자원을 낭비하면서 락 획득을 기다린다.
All virtual threads stay mounted on the initially mounted carrier threads.
So the virtual threads does not release the carrier thread and waste the resource while waiting for the lock.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment