Java21 虚拟线程在 synchronized 代码块中被 pin 住问题复现

Java juc JFR About 5,636 words

测试代码

public class VirtualThreadSynchronizedPinnedDemo {
    // 共享锁对象:所有虚拟线程会竞争这把锁
    private static final Object LOCK = new Object();
    // 倒计时器:确保所有虚拟线程同时开始竞争锁
    private static final CountDownLatch LATCH = new CountDownLatch(1);
    // 统计成功执行完 synchronized 块的虚拟线程数
    private static int completedCount = 0;

    public static void main(String[] args) throws Exception {
        // 1. 配置:创建 100 个虚拟线程(远超默认载体线程数,模拟高并发)

        Class<Thread> threadClass = Thread.class;
        Method method = threadClass.getDeclaredMethod("currentCarrierThread");
        method.setAccessible(true);

        int virtualThreadCount = 100;
        for (int i = 0; i < virtualThreadCount; i++) {
            int threadId = i;
            // 创建并启动虚拟线程
            Thread.ofVirtual().name("VT-" + threadId).start(() -> {
                try {
                    // 等待所有虚拟线程就绪,统一开始竞争锁
                    LATCH.await();

                    // 关键:进入 synchronized 块——Java 21 中会导致虚拟线程被固定
                    synchronized (LOCK) {
                        System.out.printf(LocalDateTime.now() + " [%s] [%s] 成功获取锁,开始执行(此时虚拟线程被固定)%n",
                                 method.invoke(null),  Thread.currentThread().getName());
                        // 模拟业务阻塞(如数据库查询、IO 操作)
                        Thread.sleep(1000); // 阻塞期间,虚拟线程无法卸载,载体线程被占用

                        // 执行完成,计数+1
                        completedCount++;
                        System.out.printf(LocalDateTime.now() + " [%s] [%s] 释放锁,执行完成%n", method.invoke(null), Thread.currentThread().getName());
                    }
                } catch (Exception e) {
                    Thread.currentThread().interrupt();
                    System.out.printf(LocalDateTime.now() + " [%s] 被中断%n", Thread.currentThread().getName());
                }
            });
        }

        new Thread(() -> {
            for (long i = 0; i < Long.MAX_VALUE; i++) {
                Thread.ofVirtual().start(() -> {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {

                    }
                });
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {

                }
            }
        }).start();

        System.out.println(LocalDateTime.now() + " 所有虚拟线程就绪,开始竞争锁...");
        LATCH.countDown();


    }
}

输出

2025-10-05T13:27:48.199150300 [Thread[#124,ForkJoinPool-1-worker-4,5,CarrierThreads]] [VT-0] 成功获取锁,开始执行(此时虚拟线程被固定)
2025-10-05T13:27:49.243506500 [Thread[#124,ForkJoinPool-1-worker-4,5,CarrierThreads]] [VT-0] 释放锁,执行完成
2025-10-05T13:27:49.243506500 [Thread[#22,ForkJoinPool-1-worker-1,5,CarrierThreads]] [VT-3] 成功获取锁,开始执行(此时虚拟线程被固定)
2025-10-05T13:27:50.248552900 [Thread[#22,ForkJoinPool-1-worker-1,5,CarrierThreads]] [VT-3] 释放锁,执行完成
2025-10-05T13:27:50.249250100 [Thread[#33,ForkJoinPool-1-worker-2,5,CarrierThreads]] [VT-4] 成功获取锁,开始执行(此时虚拟线程被固定)
2025-10-05T13:27:51.262913500 [Thread[#33,ForkJoinPool-1-worker-2,5,CarrierThreads]] [VT-4] 释放锁,执行完成
2025-10-05T13:27:51.263927 [Thread[#48,ForkJoinPool-1-worker-3,5,CarrierThreads]] [VT-1] 成功获取锁,开始执行(此时虚拟线程被固定)
2025-10-05T13:27:52.271131500 [Thread[#48,ForkJoinPool-1-worker-3,5,CarrierThreads]] [VT-1] 释放锁,执行完成
2025-10-05T13:27:52.271854400 [Thread[#33,ForkJoinPool-1-worker-2,5,CarrierThreads]] [VT-7] 成功获取锁,开始执行(此时虚拟线程被固定)
2025-10-05T13:27:53.274071200 [Thread[#33,ForkJoinPool-1-worker-2,5,CarrierThreads]] [VT-7] 释放锁,执行完成
2025-10-05T13:27:53.275088900 [Thread[#22,ForkJoinPool-1-worker-1,5,CarrierThreads]] [VT-6] 成功获取锁,开始执行(此时虚拟线程被固定)
2025-10-05T13:27:54.290074 [Thread[#22,ForkJoinPool-1-worker-1,5,CarrierThreads]] [VT-6] 释放锁,执行完成
2025-10-05T13:27:54.291091300 [Thread[#124,ForkJoinPool-1-worker-4,5,CarrierThreads]] [VT-5] 成功获取锁,开始执行(此时虚拟线程被固定)
2025-10-05T13:27:55.305432100 [Thread[#124,ForkJoinPool-1-worker-4,5,CarrierThreads]] [VT-5] 释放锁,执行完成
2025-10-05T13:27:55.306165 [Thread[#22,ForkJoinPool-1-worker-1,5,CarrierThreads]] [VT-10] 成功获取锁,开始执行(此时虚拟线程被固定)
2025-10-05T13:27:56.314520400 [Thread[#22,ForkJoinPool-1-worker-1,5,CarrierThreads]] [VT-10] 释放锁,执行完成
2025-10-05T13:27:56.315567900 [Thread[#33,ForkJoinPool-1-worker-2,5,CarrierThreads]] [VT-9] 成功获取锁,开始执行(此时虚拟线程被固定)
2025-10-05T13:27:57.321925100 [Thread[#33,ForkJoinPool-1-worker-2,5,CarrierThreads]] [VT-9] 释放锁,执行完成

现象

可以看到日志输出中,获取锁后sleep了一秒,但一秒后虚拟线程的载体线程还是原来的平台线程,并没有挂载到其他平台线程,所以猜测虚拟线程根本没有从平台线程上卸载。

下文使用JFR验证。

VirtualThreadPinned 事件

使用JFR查看jdk.VirtualThreadPinned事件。

启动参数

添加启动参数。

java -XX:StartFlightRecording=disk=true,dumponexit=true -jar test.jar

分析 JFR 文件

使用jfr命令打印指定事件。

jfr print --events jdk.VirtualThreadPinned hotspot-pid-13500-id-1-2025_10_05_15_06_02.jfr
jdk.VirtualThreadPinned {
  startTime = 15:05:46.101 (2025-10-05)
  duration = 1.04 s
  eventThread = "VT-1" (javaThreadId = 30, virtual)
  stackTrace = [
    java.lang.VirtualThread.parkOnCarrierThread(boolean, long) line: 675
    java.lang.VirtualThread.parkNanos(long) line: 634
    java.lang.VirtualThread.sleepNanos(long) line: 791
    java.lang.Thread.sleep(long) line: 507
    com.example.VirtualThreadSynchronizedPinnedDemo.lambda$main$0(Method) line: 47
    ...
  ]
}

jdk.VirtualThreadPinned {
  startTime = 15:05:47.145 (2025-10-05)
  duration = 1.05 s
  eventThread = "VT-3" (javaThreadId = 32, virtual)
  stackTrace = [
    java.lang.VirtualThread.parkOnCarrierThread(boolean, long) line: 675
    java.lang.VirtualThread.parkNanos(long) line: 634
    java.lang.VirtualThread.sleepNanos(long) line: 791
    java.lang.Thread.sleep(long) line: 507
    com.example.VirtualThreadSynchronizedPinnedDemo.lambda$main$0(Method) line: 47
    ...
  ]
}
Views: 4 · Posted: 2025-12-24

———         Thanks for Reading         ———

Give me a Star, Thanks:)

https://github.com/fendoudebb/LiteNote

扫描下方二维码关注公众号和小程序↓↓↓

扫描下方二维码关注公众号和小程序↓↓↓
Today In History
Browsing Refresh