java 21虚拟线程需显式启用--enable-preview,thread.ofvirtual()适用于短时精确控制任务,executors.newvirtualthreadpertaskexecutor()适合批量i/o但须手动关闭;避免synchronized和阻塞jni以防钉住carrier线程;jstack默认不显示虚拟线程栈,应配合-djdk.tracepinnedthread=full和jfr调试。

Java 21 启动时必须加 --enable-preview
虚拟线程是 Java 21 的预览特性,不是默认开启的。不加参数直接跑 Thread.ofVirtual().start() 会抛 UnsupportedOperationException,错误信息里通常带 “preview” 字样。
实操建议:
- 启动命令必须显式加上
--enable-preview,例如:java --enable-preview MyApp - 如果用 Maven,需在
maven-compiler-plugin中配置<forcejavaccompileruse>true</forcejavaccompileruse>,并设<compilerargs></compilerargs>包含-XX:+EnablePreview - IDE(如 IntelliJ)需要在运行配置的 VM options 里手动填入
--enable-preview,光改语言级别没用
Thread.ofVirtual() 和 Executors.newVirtualThreadPerTaskExecutor() 别混用
两者都创建虚拟线程,但生命周期管理和适用场景完全不同:前者适合短时、明确控制的单次任务;后者本质是无界线程池,适合大量异步 I/O 场景,但容易掩盖资源泄漏。
常见错误现象:用 Executors.newVirtualThreadPerTaskExecutor() 提交了数百个阻塞读取 HTTP 的任务,结果发现内存持续上涨,GC 压力大——因为虚拟线程虽轻量,但其栈帧、监控对象、关联的 Fiber 实例仍占堆内存,且该 executor 不自动关闭。
立即学习“Java免费学习笔记(深入)”;
实操建议:
- 短任务、需精确控制启停:用
Thread.ofVirtual().unstarted(runnable).start() - 批量 I/O 密集型任务(如爬虫、网关转发):用
Executors.newVirtualThreadPerTaskExecutor(),但务必配合try-with-resources或显式close() - 别把它当传统线程池用——它不复用线程,也不限制并发数,失控时比
newFixedThreadPool(1000)更危险
虚拟线程阻塞时不会压垮 OS 线程,但 synchronized 和 JNI 仍是瓶颈
虚拟线程的优势在于挂起/恢复由 JVM 管理,不绑定 OS 线程。但一旦进入 JVM 外部阻塞点(比如 synchronized 块、Object.wait()、或任何 JNI 调用),它就会“钉住”(pin)当前 carrier thread,导致该 OS 线程无法被其他虚拟线程复用。
性能影响明显:10 万个虚拟线程里若有 1% 钉住 carrier,就可能把默认 256 个 carrier 线程全占满,后续虚拟线程只能排队等待,吞吐骤降。
实操建议:
- 避免在虚拟线程中使用
synchronized,改用ReentrantLock(非公平模式)或无锁结构(如ConcurrentHashMap) - 数据库连接、文件读写等 I/O 操作,优先走 NIO(
AsynchronousFileChannel、CompletableFuture.supplyAsync(..., executor))而非阻塞 API - JNI 调用前确认是否可重入/非阻塞;否则考虑把该逻辑剥离到专用平台线程池执行
监控和调试时看不到虚拟线程的完整栈,jstack 默认不显示
jstack <pid></pid> 输出里,虚拟线程默认只显示为 "VirtualThread[#n]/runnable",没有 Java 栈帧,对排查死锁、慢调用几乎无效。这是最常被忽略的运维盲区。
实操建议:
- 启动时追加
-Djdk.tracePinnedThread=full,当虚拟线程被钉住时会打印警告及栈信息 - 用
jcmd <pid> VM.native_memory summary</pid>观察 carrier thread 数量是否异常增长 - JDK 21+ 推荐用
jfr start --duration=60s --settings profile -o dump.jfr录制飞行记录,然后用 JDK Mission Control 打开,能清晰看到每个虚拟线程的状态变迁和阻塞点
虚拟线程不是银弹——它简化的是线程创建和调度成本,而不是消除同步竞争或 I/O 等待。真正卡住系统的,往往还是那几行 synchronized 或一个没设超时的 HttpClient 请求。








