Java21的虚拟线程真的完美吗?

锁不是不在了,只是没人能去拿。你看着它,等着它,却永远不能碰到它。这就是虚拟线程的浪漫。

我最近的项目一律使用的 Java 21,其中一个重要原因是看中了 虚拟线程(Virtual Threads) 这一新特性。Java 21 将虚拟线程从预览阶段带入正式版,提供了一种更轻量的线程实现。据官方描述,虚拟线程是“极轻量的线程,实现高吞吐并发应用所需的编写、维护和监控成本都大幅降低”,其关键在于当发生阻塞时线程可以自动挂起并让出底层操作系统线程去执行其它任务。利用虚拟线程,我们希望既简化并发编程模型(避免繁琐的回调地狱或 Reactor 流水线),又提升系统在高并发场景下的吞吐性能。

我在重构我的一个Task Managment System基于的是最新的Spring Boot3,(写这个文章的时候,Spring Boot 4.0.1已经在两周前发布了)。该服务使用的是内嵌的Tomcat提供REST风格的接口。在Java21环境下,我手动在配置文件中开启了虚拟线程的支持,随后我直接对系统进行了一波高并发压力的测试。本以为可以看到一条完美的直线,可惜理想和现实汪往往差距很远!在有一次测试中,我发现系统吞吐量突然归零的诡异问题。

问题复现

当时压测跑着跑着,系统突然就“停跳”了。最诡异的是,应用既没报错也没宕机,但吞吐量直接掉到了零,整个 Tomcat 就像被点穴了一样,旧请求卡死不动,新请求死活进不来。

翻看后台,连一条异常日志都刷不出来,安静得让人发毛。但一查监控,我发现 CLOSE_WAIT 的连接数正疯狂往上涨。这意味着客户端早就等不及断开了,可服务器这边还死死拽着连接不撒手,根本腾不出手来去处理新任务。简单说,系统还没彻底“断气”,但已经成了无法对外服务的“植物人”状态。

img

看着监控面板上这两根走势极其离谱的曲线,我当时心里就凉了大半截。

大约到晚上 23:30 那会儿,本来跑得稳稳的吞吐量(就是那根蓝线)跟蹦极一样,直接来了个 90 度大跳水,瞬间躺平变 0 了。 与此同时,旁边的 CLOSE_WAIT 连接数(那根棕线)却像坐了火箭一样疯涨,把右侧纵轴都快撑爆了。

这画面太诡异了:系统还没挂,但已经完全“石化”了,所有请求都被死死卡在喉咙里出不来。看着这满屏的 CLOSE_WAIT,我知道系统肯定是卡在了某个环节,导致连接断不开也挪不动。没辙,只能硬着头皮赶紧上线开搞,看看这黑盒子里到底藏了什么鬼。

排查与诊断

既然服务没挂但又不理人,我的第一反应就是:坏了,肯定是有线程被锁死了,或者是哪里在大面积阻塞。

我二话没说,反手就打了一个 jstack 想看看线程栈。可等我把 Dump 文件打开一看,整个人都懵了——“一片风平浪静”。所有的工作线程都老老实实地待在空闲状态,既没有死锁报错,也没有哪里在疯狂竞争资源。看起来 JVM 就像是在优雅地“空转”,好像完全没活干一样。

那种感觉特别无力,就像你明明看到病人已经快不行了,但心电图居然显示一切正常。我当时就在那儿犯嘀咕:难不成这系统还学会“隐身”了?

冷静下来反复推敲,我才猛地拍了一下大腿:坏了,这项目可是开了虚拟线程(Virtual Threads)的!

这时候我才反应过来,传统的 jstack 工具根本抓不到虚拟线程的踪迹。在它眼里,那些真正在跑业务、被卡住的虚拟线程全都是“透明”的,我看到的那些空闲线程不过是承载它们的“搬运工”而已。

发现了这个盲区,我赶紧换了招式,改用 jcmd Thread.dump_to_file。这命令才是专门对付虚拟线程的“照妖镜”,能把那些藏在暗处的调用栈全部揪出来。顺带手,我也把堆内存(Heap Dump)给导了出来,双管齐下。

手里攥着这份“完整版”的数据,我深吸了一口气:行了,这回我看你往哪儿躲。

深入分析

首先,新的线程Dump里揭示了一个耐人寻味的现象:存在成千上百个所谓“空白”的虚拟线程,它们被创建出来后从未真正执行过任何任务,因此没有任何堆栈信息。这些虚拟线程对象大量堆积,其数量和系统中 CLOSE_WAIT 连接的数量惊人地接近。也就是说,每一个未曾运行的虚拟线程,很可能对应着一个悬而未决的 socket 连接。这暗示问题出在虚拟线程被创建后没有得到调度执行,从而请求卡住、连接未关闭。

image-20251228215546960

为什么虚拟线程全“集体罢工”了?

要搞清楚这背后的鬼天气,得先聊聊虚拟线程的“潜规则”。

大家都知道,虚拟线程不是真的操作系统线程,它更像是一堆“小零件”,由 JVM 的调度器(Fork-Join Pool)负责把它们安装到几个真正的 载体线程(Carrier Thread) 上去跑。在我的 Spring Boot 应用里,开启虚拟线程后,Tomcat 确实变猛了——来一个请求就造一个虚拟线程。按理说,虚拟线程随勾随用,遇到阻塞就该自己“闪开”,把底下的载体线程让给别人。

可理想很丰满,现实直接给我开了个大玩笑。

当我翻开那份完整的 jcmd Dump 文件时,真相简直让我大跌眼镜:我的载体线程被“绑架”了!

虽然我用的是虚拟线程,但在代码的某个角落,有人用了 synchronized 同步块。JDK 有个著名的坑:如果虚拟线程在 synchronized 块里卡住了,它就不会“礼让”出底下的载体线程,而是会死死地“钉”在上面。 这种现象官方叫“线程固定(Pinning)”。

更倒霉的是,我的服务器是 4 核的,默认也就 4 个载体线程。Dump 显示,正好有 4 个虚拟线程在竞争同一把锁时被 Pin 住了。这就好比一共就 4 个工位,被 4 个人占着位置干等,剩下的成千上万个虚拟线程只能在外面排队。 Tomcat 还在不断地接新客、造新线程,但这些线程连个落脚的工位都没有,只能在队列里“活活饿死”,直到客户端等不及把连接断了,留下满地的 CLOSE_WAIT。

到底谁拿着那把锁?

最让我头大的是,Java 21 的 jcmd Dump 有个硬伤:它不标注锁到底在谁手里。 我只看到一堆线程在排队:

  • 4 个被 Pin 住的虚拟线程:霸占了所有 CPU 资源(载体线程)。
  • 编号为 #42 的虚拟线程:虽然也在排队,但它还没抢到工位,正处于挂起状态。
  • 一个普通平台线程(#7):这货是负责后台日志异步刷新的。

这个 #7 线程的行为最诡异。它之前拿着锁去执行了一个 awaitNanos()(等待队列刷新),按理说等完了应该重新拿回锁继续干活。可当时的情况是:它想回也回不来了。

为了破案,我不得不祭出 Eclipse MAT 去翻堆内存 Dump,直接查锁对象的底层状态。

这一查,更有意思的事情发生了:锁其实是空的! 内存显示锁的 state 是 0,exclusiveOwnerThread 也是 null。也就是说,压根没人占着锁!而排在队列最前面的正是那个挂起的虚拟线程 #42

按逻辑,#42 已经被唤醒准备去拿锁了,但问题就在这里:#42 要想去拿锁,它得先被调度到一个载体线程上运行。可现在所有的载体线程都被那 4 个 Pin 住的倒霉蛋给占满了!

破案了:这成了一个死循环。 #42 等着载体线程去拿锁,而载体线程被另外几个等锁的家伙死死占着不放(因为他们被 synchronized 固定住了)。大家谁也别想动,系统就这么在沉默中彻底“石化”了。

image-20251228220101878

这一通分析下来,我总算抓住了这个“幕后黑手”。

说实话,理论上锁从释放到被下一个线程接手,也就那么一瞬间的事。可偏偏在那个节骨眼上,事情卡在了一个极度尴尬的闭环里:线程 #42 虽然收到了“锁已释放”的信号,但因为它是个虚拟线程,它得先找个“工位”(载体线程)坐下来才能去伸手拿锁。

可这时候,全公司仅有的 4 个工位,全被那 4 个卡在 synchronized 里的兄弟给占死了。这 4 位仁兄也在等锁释放,好赶紧干完活儿走人腾位置。

这就是最诡异的地方:锁是空的,没人拿;想拿锁的人(#42)没位子坐,动不了;占着位子的人(那 4 个被 Pin 住的线程)又在等锁。

大家大眼瞪小眼,谁也动弹不得。这哪是普通的死锁啊?这简直是**“一把空锁 + 四个坑位”**诱发的另类死锁。平时我们防着两个锁互相等,这次倒好,锁在等线程,线程在等锁,活生生把系统憋成了“植物人”。

解决措施:给虚拟线程“松绑”

既然找到了症结,剩下的就是动刀子了。既然 synchronized 会把虚拟线程钉死在载体线程上,那我就得让它“解绑”。

我的方案其实很简单,但很管用:

  1. 全面“拆迁” synchronized:我把代码里所有涉及长时间阻塞、I/O 操作或者等待条件的 synchronized 块全部铲掉。
  2. 换上 ReentrantLock:改用 java.util.concurrent.Lock 系列的显式锁。
    • 为什么这么做? 因为 ReentrantLock 底层用的是 LockSupport 挂起机制,它对虚拟线程非常友好。当虚拟线程在等待 ReentrantLock 时,它会很自觉地把底下的载体线程让出来,绝不“占着茅坑不拉屎”。
  3. 优化临界区:确保所有可能磨叽的调用(比如网络请求、休眠等)都尽量挪到锁范围之外。

重构完代码重新部署后,我心里其实也有点打鼓。但随后的压测数据说明了一切:那根代表吞吐量的蓝线再也没有“蹦极”过,CLOSE_WAIT 也不再乱窜。 这次经历也算给我提了个醒:虚拟线程虽然是个好东西,但要是带着旧时代的 synchronized 思维去玩,它真的会分分钟教你做人。

img

反思与展望

这次压测翻车也算是给我上了一课:Java 21 的虚拟线程虽然香,但它和老牌的 synchronized 锁之间确实存在“先天性八字不合”。

虚拟线程的设计初衷是让我们能用写同步代码的方式,跑出异步的高性能,但这玩意儿就像一把双刃剑——如果你还没搞清楚什么是 Pinning(线程固定) 就盲目上线,它能让系统陷入比以前更隐蔽、更难排查的死锁泥潭。

不过,大家也别因为这一个坑就对虚拟线程失去信心。作为一项新技术,它的生态确实还在“打补丁”的过程中。我们在享受它带来的高并发红利时,确实得对现阶段的局限性保持警惕,老老实实按照官方建议把那些老旧的锁机制改一改。

往长远了看,这个锅终究得 JVM 自己来背。好消息是,社区已经意识到了这个问题,并开始在底层动手术了。

据说即将到来的 Java 24(通过 JEP 491)将彻底重构虚拟线程对同步锁的处理机制。到那时候,就算虚拟线程进了 synchronized 块被卡住,它也能像处理其他阻塞一样,大大方方地把底下的载体线程释放出来。这意味着,像我们这次遇到的这种“另类死锁”将从底层被彻底杜绝。

虚拟线程的完全体依然值得期待。但在那一天到来之前,答应我,先把代码里的 synchronized 搜一搜,能换的就先换了吧!

image-20251228220417342

附录:

示例代码:

import java.time.Duration;
import java.util.concurrent.Executors;

public class VirtualThreadPinningDemo {
    // 一个共享资源,我们将使用 synchronized 来保护它
    private static final Object sharedLock = new Object();

    public static void main(String[] args) {
        // 使用虚拟线程执行器创建大量虚拟线程
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 100; i++) {
                int taskId = i;
                executor.submit(() -> performTask(taskId));
            }
        }
        System.out.println("All tasks completed.");
    }

    private static void performTask(int id) {
        System.out.printf("Task %d started on thread: %s%n", id, Thread.currentThread());

        // 情况1:在 synchronized 块内进行阻塞操作 -> 会导致 PINNED
        synchronized (sharedLock) {
            System.out.printf("Task %d acquired lock. Thread: %s%n", id, Thread.currentThread());
            try {
                // 模拟一个阻塞操作,比如睡眠或I/O
                // 在 synchronized 块内睡眠,虚拟线程无法被卸载,平台线程被占用!
                Thread.sleep(Duration.ofSeconds(2));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.printf("Task %d released lock.%n", id);
        }
        System.out.printf("Task %d finished.%n", id);
    }
}
Task 0 started on thread: VirtualThread[#30]/runnable@ForkJoinPool-1-worker-1
Task 0 acquired lock. Thread: VirtualThread[#30]/runnable@ForkJoinPool-1-worker-1
Task 1 started on thread: VirtualThread[#32]/runnable@ForkJoinPool-1-worker-10
Task 4 started on thread: VirtualThread[#35]/runnable@ForkJoinPool-1-worker-9
Task 5 started on thread: VirtualThread[#36]/runnable@ForkJoinPool-1-worker-5
Task 6 started on thread: VirtualThread[#37]/runnable@ForkJoinPool-1-worker-6
Task 7 started on thread: VirtualThread[#38]/runnable@ForkJoinPool-1-worker-8
Task 2 started on thread: VirtualThread[#33]/runnable@ForkJoinPool-1-worker-3
Task 8 started on thread: VirtualThread[#39]/runnable@ForkJoinPool-1-worker-7
Task 9 started on thread: VirtualThread[#40]/runnable@ForkJoinPool-1-worker-2
Task 10 started on thread: VirtualThread[#41]/runnable@ForkJoinPool-1-worker-4
Task 11 started on thread: VirtualThread[#42]/runnable@ForkJoinPool-1-worker-11
Task 12 started on thread: VirtualThread[#43]/runnable@ForkJoinPool-1-worker-13
Task 14 started on thread: VirtualThread[#45]/runnable@ForkJoinPool-1-worker-14
Task 13 started on thread: VirtualThread[#44]/runnable@ForkJoinPool-1-worker-12
Task 15 started on thread: VirtualThread[#46]/runnable@ForkJoinPool-1-worker-15
Task 16 started on thread: VirtualThread[#47]/runnable@ForkJoinPool-1-worker-16
Task 0 released lock.
Task 0 finished.
Task 17 started on thread: VirtualThread[#48]/runnable@ForkJoinPool-1-worker-1
这里就卡住不向下走了

但是同样的代码,在JDK 25下,运行正常:

Task 2 started on thread: VirtualThread[#42]/runnable@ForkJoinPool-1-worker-3
Task 2 acquired lock. Thread: VirtualThread[#42]/runnable@ForkJoinPool-1-worker-3
Task 0 started on thread: VirtualThread[#37]/runnable@ForkJoinPool-1-worker-6
Task 3 started on thread: VirtualThread[#45]/runnable@ForkJoinPool-1-worker-6
Task 1 started on thread: VirtualThread[#39]/runnable@ForkJoinPool-1-worker-6
Task 4 started on thread: VirtualThread[#47]/runnable@ForkJoinPool-1-worker-6
Task 5 started on thread: VirtualThread[#48]/runnable@ForkJoinPool-1-worker-6
Task 7 started on thread: VirtualThread[#50]/runnable@ForkJoinPool-1-worker-6
Task 8 started on thread: VirtualThread[#53]/runnable@ForkJoinPool-1-worker-6
Task 9 started on thread: VirtualThread[#54]/runnable@ForkJoinPool-1-worker-6
Task 10 started on thread: VirtualThread[#55]/runnable@ForkJoinPool-1-worker-6
Task 11 started on thread: VirtualThread[#56]/runnable@ForkJoinPool-1-worker-6
Task 12 started on thread: VirtualThread[#57]/runnable@ForkJoinPool-1-worker-6
Task 13 started on thread: VirtualThread[#58]/runnable@ForkJoinPool-1-worker-6
Task 15 started on thread: VirtualThread[#60]/runnable@ForkJoinPool-1-worker-6
Task 16 started on thread: VirtualThread[#61]/runnable@ForkJoinPool-1-worker-6
Task 14 started on thread: VirtualThread[#59]/runnable@ForkJoinPool-1-worker-6
Task 17 started on thread: VirtualThread[#62]/runnable@ForkJoinPool-1-worker-6
Task 19 started on thread: VirtualThread[#64]/runnable@ForkJoinPool-1-worker-6
Task 18 started on thread: VirtualThread[#63]/runnable@ForkJoinPool-1-worker-6
Task 6 started on thread: VirtualThread[#49]/runnable@ForkJoinPool-1-worker-6
Task 21 started on thread: VirtualThread[#67]/runnable@ForkJoinPool-1-worker-6
Task 20 started on thread: VirtualThread[#66]/runnable@ForkJoinPool-1-worker-6
Task 22 started on thread: VirtualThread[#68]/runnable@ForkJoinPool-1-worker-6
Task 23 started on thread: VirtualThread[#69]/runnable@ForkJoinPool-1-worker-6
Task 24 started on thread: VirtualThread[#70]/runnable@ForkJoinPool-1-worker-6
Task 26 started on thread: VirtualThread[#72]/runnable@ForkJoinPool-1-worker-6
Task 25 started on thread: VirtualThread[#71]/runnable@ForkJoinPool-1-worker-6
Task 27 started on thread: VirtualThread[#73]/runnable@ForkJoinPool-1-worker-6
Task 28 started on thread: VirtualThread[#74]/runnable@ForkJoinPool-1-worker-3
Task 31 started on thread: VirtualThread[#77]/runnable@ForkJoinPool-1-worker-7
Task 30 started on thread: VirtualThread[#76]/runnable@ForkJoinPool-1-worker-7
Task 32 started on thread: VirtualThread[#78]/runnable@ForkJoinPool-1-worker-7
Task 29 started on thread: VirtualThread[#75]/runnable@ForkJoinPool-1-worker-7
Task 33 started on thread: VirtualThread[#79]/runnable@ForkJoinPool-1-worker-7
Task 34 started on thread: VirtualThread[#80]/runnable@ForkJoinPool-1-worker-7
Task 35 started on thread: VirtualThread[#81]/runnable@ForkJoinPool-1-worker-7
Task 36 started on thread: VirtualThread[#82]/runnable@ForkJoinPool-1-worker-7
Task 37 started on thread: VirtualThread[#83]/runnable@ForkJoinPool-1-worker-7
Task 38 started on thread: VirtualThread[#84]/runnable@ForkJoinPool-1-worker-7
Task 39 started on thread: VirtualThread[#85]/runnable@ForkJoinPool-1-worker-7
Task 41 started on thread: VirtualThread[#87]/runnable@ForkJoinPool-1-worker-7
Task 42 started on thread: VirtualThread[#88]/runnable@ForkJoinPool-1-worker-7
Task 40 started on thread: VirtualThread[#86]/runnable@ForkJoinPool-1-worker-7
Task 43 started on thread: VirtualThread[#89]/runnable@ForkJoinPool-1-worker-7
Task 44 started on thread: VirtualThread[#90]/runnable@ForkJoinPool-1-worker-7
Task 45 started on thread: VirtualThread[#91]/runnable@ForkJoinPool-1-worker-7
Task 46 started on thread: VirtualThread[#92]/runnable@ForkJoinPool-1-worker-7
Task 47 started on thread: VirtualThread[#93]/runnable@ForkJoinPool-1-worker-7
Task 48 started on thread: VirtualThread[#94]/runnable@ForkJoinPool-1-worker-7
Task 49 started on thread: VirtualThread[#95]/runnable@ForkJoinPool-1-worker-7
Task 50 started on thread: VirtualThread[#96]/runnable@ForkJoinPool-1-worker-7
Task 51 started on thread: VirtualThread[#97]/runnable@ForkJoinPool-1-worker-7
Task 52 started on thread: VirtualThread[#98]/runnable@ForkJoinPool-1-worker-7
Task 53 started on thread: VirtualThread[#99]/runnable@ForkJoinPool-1-worker-7
Task 54 started on thread: VirtualThread[#100]/runnable@ForkJoinPool-1-worker-7
Task 55 started on thread: VirtualThread[#101]/runnable@ForkJoinPool-1-worker-7
Task 56 started on thread: VirtualThread[#102]/runnable@ForkJoinPool-1-worker-7
Task 58 started on thread: VirtualThread[#104]/runnable@ForkJoinPool-1-worker-7
Task 59 started on thread: VirtualThread[#105]/runnable@ForkJoinPool-1-worker-7
Task 57 started on thread: VirtualThread[#103]/runnable@ForkJoinPool-1-worker-7
Task 60 started on thread: VirtualThread[#106]/runnable@ForkJoinPool-1-worker-7
Task 62 started on thread: VirtualThread[#108]/runnable@ForkJoinPool-1-worker-7
Task 63 started on thread: VirtualThread[#109]/runnable@ForkJoinPool-1-worker-7
Task 61 started on thread: VirtualThread[#107]/runnable@ForkJoinPool-1-worker-7
Task 64 started on thread: VirtualThread[#110]/runnable@ForkJoinPool-1-worker-7
Task 66 started on thread: VirtualThread[#112]/runnable@ForkJoinPool-1-worker-7
Task 65 started on thread: VirtualThread[#111]/runnable@ForkJoinPool-1-worker-7
Task 67 started on thread: VirtualThread[#113]/runnable@ForkJoinPool-1-worker-7
Task 68 started on thread: VirtualThread[#114]/runnable@ForkJoinPool-1-worker-7
Task 69 started on thread: VirtualThread[#115]/runnable@ForkJoinPool-1-worker-7
Task 70 started on thread: VirtualThread[#116]/runnable@ForkJoinPool-1-worker-7
Task 71 started on thread: VirtualThread[#117]/runnable@ForkJoinPool-1-worker-7
Task 72 started on thread: VirtualThread[#118]/runnable@ForkJoinPool-1-worker-7
Task 73 started on thread: VirtualThread[#119]/runnable@ForkJoinPool-1-worker-7
Task 74 started on thread: VirtualThread[#120]/runnable@ForkJoinPool-1-worker-7
Task 75 started on thread: VirtualThread[#121]/runnable@ForkJoinPool-1-worker-7
Task 76 started on thread: VirtualThread[#122]/runnable@ForkJoinPool-1-worker-7
Task 77 started on thread: VirtualThread[#123]/runnable@ForkJoinPool-1-worker-7
Task 78 started on thread: VirtualThread[#124]/runnable@ForkJoinPool-1-worker-7
Task 79 started on thread: VirtualThread[#125]/runnable@ForkJoinPool-1-worker-7
Task 80 started on thread: VirtualThread[#126]/runnable@ForkJoinPool-1-worker-7
Task 81 started on thread: VirtualThread[#127]/runnable@ForkJoinPool-1-worker-7
Task 82 started on thread: VirtualThread[#128]/runnable@ForkJoinPool-1-worker-7
Task 83 started on thread: VirtualThread[#129]/runnable@ForkJoinPool-1-worker-7
Task 84 started on thread: VirtualThread[#130]/runnable@ForkJoinPool-1-worker-7
Task 85 started on thread: VirtualThread[#131]/runnable@ForkJoinPool-1-worker-7
Task 86 started on thread: VirtualThread[#132]/runnable@ForkJoinPool-1-worker-7
Task 88 started on thread: VirtualThread[#134]/runnable@ForkJoinPool-1-worker-7
Task 87 started on thread: VirtualThread[#133]/runnable@ForkJoinPool-1-worker-7
Task 89 started on thread: VirtualThread[#135]/runnable@ForkJoinPool-1-worker-7
Task 90 started on thread: VirtualThread[#136]/runnable@ForkJoinPool-1-worker-7
Task 91 started on thread: VirtualThread[#137]/runnable@ForkJoinPool-1-worker-7
Task 92 started on thread: VirtualThread[#138]/runnable@ForkJoinPool-1-worker-7
Task 93 started on thread: VirtualThread[#139]/runnable@ForkJoinPool-1-worker-7
Task 94 started on thread: VirtualThread[#140]/runnable@ForkJoinPool-1-worker-7
Task 95 started on thread: VirtualThread[#141]/runnable@ForkJoinPool-1-worker-8
Task 97 started on thread: VirtualThread[#143]/runnable@ForkJoinPool-1-worker-8
Task 96 started on thread: VirtualThread[#142]/runnable@ForkJoinPool-1-worker-8
Task 98 started on thread: VirtualThread[#144]/runnable@ForkJoinPool-1-worker-3
Task 99 started on thread: VirtualThread[#145]/runnable@ForkJoinPool-1-worker-3
Task 2 released lock.
Task 2 finished.
Task 0 acquired lock. Thread: VirtualThread[#37]/runnable@ForkJoinPool-1-worker-7
Task 0 released lock.
Task 0 finished.
Task 3 acquired lock. Thread: VirtualThread[#45]/runnable@ForkJoinPool-1-worker-5
Task 3 released lock.
Task 3 finished.
Task 1 acquired lock. Thread: VirtualThread[#39]/runnable@ForkJoinPool-1-worker-7
Task 1 released lock.
Task 1 finished.
Task 4 acquired lock. Thread: VirtualThread[#47]/runnable@ForkJoinPool-1-worker-5
Task 4 released lock.
Task 4 finished.
Task 5 acquired lock. Thread: VirtualThread[#48]/runnable@ForkJoinPool-1-worker-7
Task 5 released lock.
Task 5 finished.
Task 7 acquired lock. Thread: VirtualThread[#50]/runnable@ForkJoinPool-1-worker-8
Task 7 released lock.
Task 7 finished.
Task 8 acquired lock. Thread: VirtualThread[#53]/runnable@ForkJoinPool-1-worker-8
Task 8 released lock.
Task 8 finished.
Task 9 acquired lock. Thread: VirtualThread[#54]/runnable@ForkJoinPool-1-worker-6
Task 9 released lock.
Task 9 finished.
Task 10 acquired lock. Thread: VirtualThread[#55]/runnable@ForkJoinPool-1-worker-8
Task 10 released lock.
Task 10 finished.
Task 11 acquired lock. Thread: VirtualThread[#56]/runnable@ForkJoinPool-1-worker-2
Task 11 released lock.
Task 11 finished.
Task 12 acquired lock. Thread: VirtualThread[#57]/runnable@ForkJoinPool-1-worker-8
Task 12 released lock.
Task 12 finished.
Task 13 acquired lock. Thread: VirtualThread[#58]/runnable@ForkJoinPool-1-worker-7
Task 13 released lock.
Task 13 finished.
Task 15 acquired lock. Thread: VirtualThread[#60]/runnable@ForkJoinPool-1-worker-7
Task 15 released lock.
Task 15 finished.
Task 16 acquired lock. Thread: VirtualThread[#61]/runnable@ForkJoinPool-1-worker-5
Task 16 released lock.
Task 16 finished.
Task 14 acquired lock. Thread: VirtualThread[#59]/runnable@ForkJoinPool-1-worker-2
Task 14 released lock.
Task 14 finished.
Task 17 acquired lock. Thread: VirtualThread[#62]/runnable@ForkJoinPool-1-worker-8
Task 17 released lock.
Task 17 finished.
Task 19 acquired lock. Thread: VirtualThread[#64]/runnable@ForkJoinPool-1-worker-8
Task 19 released lock.
Task 19 finished.
Task 18 acquired lock. Thread: VirtualThread[#63]/runnable@ForkJoinPool-1-worker-2
Task 18 released lock.
Task 18 finished.
... 会一直正常执行,直到运行结束