Java并发编程面试题

JDK21的特性,虚拟线程,虚拟线程与普通线程的区别?

核心点

虚拟线程(Virtual Threads) 是 JDK 21 正式推出的轻量级用户态线程(由 JVM 调度),而普通线程(平台线程 Platform Threads) 是操作系统内核线程的 1:1 封装。

核心区别在于:普通线程是“重型操作系统资源”,创建慢、占内存大(约1MB)、阻塞时会占用昂贵的 OS 内核;而虚拟线程是“JVM 管理的普通 Java 对象”,创建极快、占内存极小(KB级)、阻塞时会自动让出底层载体线程,从而支持百万级并发。

底层核心原理

  1. 载体线程(Carrier Thread):JVM 内部维护一个数量极少的平台线程池(默认与 CPU 核心数相当)作为“载体”。
  2. 挂载(Mount):当虚拟线程开始执行时,JVM 将其临时绑定到一个空闲的载体线程上,此时载体线程运行该虚拟线程的代码。
  3. 卸载(Unmount):当虚拟线程执行到阻塞操作(如读取数据库、远程调用)时,JVM 会将其从载体线程上剥离,虚拟线程的状态保留在堆内存中,而载体线程立刻被释放去执行另一个可运行的虚拟线程。
  4. 恢复(Resume):当阻塞操作完成(数据返回),JVM 会将这个虚拟线程重新挂载到某个空闲的载体线程上,继续执行。

本质结论:虚拟线程消除了“阻塞导致内核线程闲置”的痛点,让开发者可以用同步阻塞的编程风格,写出异步非阻塞的高吞吐代码

面试官高频追问

追问1:虚拟线程这么快,为什么不用来替代所有普通线程?有什么坑吗?

  1. 池化无意义:不要对虚拟线程使用线程池(如 newFixedThreadPool),因为虚拟线程创建极其廉价,池化反而限制了吞吐。应使用 Executors.newVirtualThreadPerTaskExecutor(),每个任务新建一个。
  2. “Pinning(钉住)”问题:如果虚拟线程在 synchronized 同步块内执行阻塞操作,JVM 无法将其卸载(为了兼容性),会钉死载体线程,导致吞吐下降。**最佳实践是用 ReentrantLock 替代 synchronized**。

追问2:既然虚拟线程解决了阻塞问题,那是不是可以把所有 Tomcat/Nginx 的线程模型都换成虚拟线程?
👉 :对于 IO 密集型(业务逻辑涉及大量数据库、RPC 调用)的应用,效果极佳,QPS 可大幅提升。但对于 CPU 密集型(大量数学运算、加密解密)的应用,虚拟线程几乎没有优势,因为 CPU 核数才是瓶颈,且过多的虚拟线程反而会增加 JVM 调度开销。最佳实践是:IO 密集型用虚拟线程,CPU 密集型保持平台线程或限制虚拟线程数量。

追问3:你说 JVM 调度虚拟线程,那它和 Go 语言的 Goroutine 有何本质区别?
👉 :核心区别在于调度时机。Go 的 Goroutine 是抢占式的(有全局调度器监控,长时间运行会主动让出)。而 JVM 的虚拟线程目前是协作式(非抢占式) 的,它只在阻塞点(I/O、锁、sleep)时让出载体线程。如果虚拟线程在死循环做纯计算且没有阻塞点,它会永久占住载体线程,导致其他虚拟线程饥饿。因此,在处理长耗时纯计算任务时需格外小心。

总结

“虚拟线程不是让我们去‘管理’并发,而是让我们‘忘记’并发管理。它解决了长期以来 Java 在微服务和云原生场景下‘高并发导致线程耗尽’的痛点。在实际落地中,我主张保留少量普通线程池处理 CPU 密集任务,而将全部 IO 交互层(Web 容器、RPC 调用)替换为虚拟线程,用最廉价的阻塞写法实现最高效的异步性能。

ConcurrentHashMap在1.7和1.8区别

--- 本文结束 The End ---