本文主要介绍Java传统的线程和虚拟线程的特点和区别,以及虚拟线程的编码方法和注意事项。
传统的线程
在旧的Java版本中使用的线程依赖于操作系统的线程,创建线程、销毁线程以及线程切换都需要大量性能开销。而操作系统的线程数有限,当应用系统需要大量线程的时候,可能会导致系统资源耗竭,性能下降,甚至导致系统奔溃。在旧的Java版本中,我们所使用java.lang.Thread来定义线程,这个就是由操作系统所支持的线程。这种线程通常以1:1的比例映射到OS调度的内核。OS线程相当“重”。根据操作系统配置,默认情况下,每个线程消耗2到10 MB, 因此,如果想在发应用程序中使用一百万个线程,那么就要求有超过2TB的内存可供使用!很明显,这就限制了线程数量。
在基于Java的Web应用中,每个请求使用一个线程有很多优点,比如状态管理和清理更加容易。但它也造成了可扩展性的限制。容易使CPU或网络资源耗尽。
虚拟线程
Java21引入虚拟线程,使得Java应用程序的线程不再受制于操作系统,可以在应用中创建多达数十亿的线程,更好地适应各种高并发场景,提供更高的并发能力。虚拟线程具有以下优点:
- 更高的性能:虚拟线程不再受制于操作系统的线程数,并且减少了线程创建、销毁、共享等操作的性能开销。从而获得更高的并发性能。
- 更高可伸缩性:虚拟线程可以创建多达数十亿的线程,更能适应Java应用的大规模并发场景。
- 资源消耗更低:虚拟线程比操作系统的线程更加轻量级,资源利用率较高,CPU和内存占用较少。
虚拟线程是一个java.lang.Thread变体,是Project Loom的一部分,不受操作系统的管理或调度,而是由JVM负责调度。当然,任何底层的逻辑都还必须在操作系统线程中运行,只是JVM利用载体线程(carrier threads,也就是平台线程)之上“携带”虚拟线程。
编码示例
虚拟线程的学习成本比较低,只需要像对待非虚拟线程一样对待他们就可以了。
(1) 传统线程的开发传统线程的用法在使用虚拟线程之前我们先来回顾一下传统的线程的写法。
Runnable fn = () -> {
// 业务代码
};
Thread thread = new Thread(fn).start();
Project Loom 简化了并发方法,它提供了一种新方法来创建平台的线程:
Thread thread = Thread.ofPlatform().
.start(runnable);
或者:
Thread thread = Thread.ofPlatform().
.daemon()
.name("my-custom-thread")
(2) 虚拟线程的用法
API写法:
Runnable fn = () -> {
// 业务代码
};
Thread thread = Thread.ofVirtual(fn)
.start();
Project Loom 写法:
Thread thread = Thread.startVirtualThread(() -> {
// 业务代码
});
创建虚拟线程的另一种方法是使用Executor:
var executorService = Executors.newVirtualThreadPerTaskExecutor();
executorService.submit(() -> {
// 业务代码
});
因为所有的虚拟线程都是守护线程,所以如果想在主线程上等待,就需要调用join()方法,Join方法的作用就是让主线程等待,当有新的线程加入时,主线程会进入等待状态,一直到调用方法的副线程执行结束为止。
thread.join();
虚拟线程开发注意事项
- 注意控制线程数:虚拟线程可以创建大量线程,很容易让开发人员不在意其数量,而过多的线程仍然会导致性能下降或资源耗尽。因此,仍需根据资源数量合理控制应用程序的并发度。
- 注意线程安全:使用虚拟线程时要注意线程安全性和正确性,避免共享可变状态、根据需要使用同步机制。
- 注意代码迁移:在从传统线程迁移到使用虚拟线程的时候,需要注意代码与新环境、新规范、新需求的一致性。
总结
虚拟线程是Java并发开发方面的通用、强大的新方法,在Java21版本中已经十分成熟了。对于需要从旧版本JDK迁移到新版本JDK的应用程序来说,改造难度并不大,同时还可以充分利用所有可用硬件资源,提高Java应用程序的并发性和可伸缩性。