虚拟线程和普通线程的区别
虚拟线程
虚拟线程(Virtual Threads)是 Java 21 中引入的一项革命性特性,它旨在从根本上改变我们处理高并发应用程序的方式。
简单来说,它们的优势和区别可以从一个比喻开始:
- 平台线程(普通线程)就像真正的工人。操作系统招募一个工人(创建线程),这个工人会亲自去完成一项任务(执行代码)。工人数量有限(一台机器最多几千个),而且招募和开除工人(创建和销毁线程)开销很大。
- 虚拟线程就像一份份任务清单。一个工人(平台线程)可以拿着许多份任务清单(虚拟线程)。当某份清单上的任务需要等待(比如等网络响应、等数据库查询),工人就把它暂时放到一边,拿起另一份清单继续工作。一个工人可以高效地处理成千上万份清单。
下面我们来详细对比它们的优势和区别。
核心优势对比表
特性 | 平台线程(普通线程) | 虚拟线程 |
---|---|---|
本质 | 操作系统线程的包装器(1:1 映射)。是重量级的资源。 | Java 运行时管理的轻量级用户态线程(M:N 映射)。 |
资源开销 | 大。每个线程都有自己的大栈(默认为1MB)、PC寄存器等。受操作系统限制。 | 极小。初始栈很小(约几百字节),按需扩容。 |
数量限制 | 有限(通常几千个)。受限于内存和操作系统配置。创建太多会耗尽内存或导致线程切换开销巨大。 | 海量(轻松数百万个)。只受限于堆内存大小,可以创建极其大量的虚拟线程。 |
创建与销毁 | 昂贵。创建和销毁成本高,通常需要池化(如使用 ExecutorService )来避免开销。 | 廉价。创建和销毁成本极低,通常不需要池化。可以为每个任务创建一个新的虚拟线程。 |
阻塞开销 | 极其昂贵。当一个平台线程阻塞(如等待I/O)时,这个宝贵的系统资源就被挂起,什么也不做,但依然占用着内存和CPU调度资源。 | 几乎为零。当虚拟线程阻塞时,它会自动从平台线程上卸载(yield),这个平台线程立即可以去执行其他就绪的虚拟线程。阻塞的操作完成后,虚拟线程再被调度到某个平台线程上继续执行。 |
调试与监控 | 传统工具(如线程转储、JProfiler)工作良好。 | 需要支持虚拟线程的工具(新版JDK工具已支持)。线程转储可以包含数百万个虚拟线程,但提供了新的格式(jcmd <pid> Thread.dump_to_file -format=json <file> )来管理。 |
适用场景 | CPU密集型计算(充分利用多核)。 | 高并发I/O密集型任务(Web服务器、微服务、数据库调用、消息队列等)。这是虚拟线程的主场。 |
虚拟线程的三大核心优势
-
极高的并发规模与资源效率
- 问题:在传统的“一个请求一个线程”的Web服务器模型中,线程数限制了并发连接数。1万个并发请求就需要1万个线程,这会给操作系统带来巨大压力。
- 解决方案:使用虚拟线程,你可以用少量平台线程(如CPU核心数)来处理数百万个并发连接。每个连接都可以分配一个虚拟线程,当它们等待网络I/O时,会自动让出执行权,从而让宝贵的平台线程始终处于工作状态。
-
简化的编程模型(告别回调地狱)
- 问题:为了应对平台线程的瓶颈,开发人员采用了异步编程(如
CompletableFuture
)或反应式编程(如 Project Reactor)。这些模式性能很好,但代码难以编写、调试和维护(俗称“回调地狱”)。 - 解决方案:虚拟线程允许你用简单的同步、阻塞式的代码风格,写出高性能的异步应用程序。你不再需要学习复杂的异步API,只需像写最传统的代码一样,自然地使用
Thread.sleep()
、synchronized
、阻塞式I/O等,而底层由虚拟线程帮你高效地处理阻塞。
代码对比示例:处理多个HTTP请求
-
传统方式(使用平台线程池):
java
// 线程池大小有限,大量请求会被排队或拒绝 ExecutorService executor = Executors.newFixedThreadPool(200); for (String url : urls) {executor.submit(() -> {// 阻塞操作,占用一个宝贵的线程String result = HttpClient.newHttpClient().send(request, BodyHandlers.ofString());process(result);}); }
-
虚拟线程方式:
java
// 为每个任务创建一个虚拟线程,成本极低 for (String url : urls) {Thread.ofVirtual().start(() -> {// 写法是同步阻塞的,但底层是非阻塞的!String result = HttpClient.newHttpClient().send(request, BodyHandlers.ofString());process(result);}); }
上面的代码看起来像是创建了成千上万个线程,但实际上只使用了少量平台线程,效率极高。
- 问题:为了应对平台线程的瓶颈,开发人员采用了异步编程(如
-
与现有代码的完美兼容性
- 虚拟线程是
Thread
类的实现。这意味着绝大多数现有的Java代码和库(如JDBC、HTTP客户端、同步代码块)无需任何修改就能在虚拟线程上运行并立即获益。这降低了 adoption 的成本。
- 虚拟线程是
需要注意的地方(并非银弹)
- 不适用于CPU密集型任务:如果你的任务是纯计算、持续占用CPU的,那么虚拟线程不会带来性能提升。此时,平台线程的数量仍然应该约等于CPU核心数,以最大化计算能力。虚拟线程的优势在于等待(I/O、锁、睡眠等)。
- 对原生代码和
synchronized
的考量:如果一个虚拟线程在synchronized
块内或执行本地(JNI)代码时被阻塞,它所承载的平台线程也会被阻塞(这被称为“引脚”,pinning)。虽然新版JDK也在优化这一点,但大量使用synchronized
可能会限制虚拟线程的可扩展性。建议优先使用java.util.concurrent
包中的锁(如ReentrantLock
),它们在虚拟线程中能够正确地被卸载。
总结
方面 | 结论 |
---|---|
核心优势 | 用同步的代码,获异步的性能。极大地提升了I/O密集型应用的并发能力、资源利用率和开发效率。 |
变革性 | 它让Java在高并发编程领域重新变得简单,有望取代许多复杂的异步编程模式,回归到直观的指令式编程。 |
使用建议 | 对于新的I/O密集型项目,强烈建议使用虚拟线程。对于现有项目,可以几乎无成本地尝试将其线程池替换为虚拟线程,以获得潜在的巨大性能提升。 |
可以将虚拟线程理解为Java为现代云原生和微服务架构提供的一项“基础设施”,它让开发者不再需要为了性能而牺牲代码的可读性和可维护性。