JDK 21 中引入的虚拟线程是 Java 并发生态系统的一个重要里程碑。本文将介绍虚拟线程的基础知识和最佳实践。
什么是虚拟线程?
多线程 是业界广泛用于开发基于 Java 的应用程序的特性。它允许我们并行运行操作,从而加快任务执行速度。任何 Java 应用程序创建的线程数量都受限于 操作系统能够处理的并行操作数量 ;换句话说,Java 应用程序中的线程数量等于操作系统线程的数量。考虑到当前快速发展的生态系统,这一限制一直是应用程序进一步扩展的瓶颈。
为了克服这一限制,Java 在 JDK 21 中引入了虚拟线程 的概念。Java 应用程序创建一个虚拟线程,该线程不与任何操作系统线程关联。这意味着每个虚拟线程都不需要依赖于平台线程(即操作系统线程) 。虚拟线程可以独立处理任何任务,并且仅在需要执行 I/O 操作时才会获取平台线程。
这种获取和释放平台线程的机制使应用程序能够灵活地创建尽可能多的虚拟线程,从而实现高并发性。
1,JDK 21 之前的线程
在 JDK 21 之前,所有 java.lang.Thread 类的实例都是操作系统线程,也称为平台线程。
这意味着在 JDK 环境中创建的每个线程都必须映射到一个平台线程。这种机制限制了 JVM 环境中可创建的线程数量。由于创建平台线程的成本很高,过去通常会使用线程池来避免重复创建线程,但这会增加应用程序的性能开销。
2,JDK 21 之后的线程
在 JDK 21 中,应用程序开发人员可以选择使用 Thread.ofVirtual() API 或 Executors.newVirtualThreadPerTaskExecutor() 创建虚拟线程,而不是平台线程。
以这种方式创建的线程在 JVM 内部运行,不会占用任何操作系统线程。虚拟线程可以像平台线程一样执行所有并发任务。虚拟线程需要平台线程来执行 I/O 操作,I/O 操作完成后,虚拟线程会释放平台线程。虚拟线程不需要线程池管理。由于它们是 JVM 内部的,因此系统中可以拥有无限数量的虚拟线程。
为什么需要虚拟线程?
-
高吞吐量:包含大量并发操作的任务(例如服务器应用程序)会花费大量时间等待。Web 服务器通常需要处理大量客户端请求。如果没有虚拟线程,它们处理并行请求的能力将受到限制。使用虚拟线程可以处理大量并发请求,从而提升服务器的处理能力。
-
无需线程池:虚拟线程资源占用低,可用性高,因此无需使用线程池。对于每个并发任务,我们都可以创建一个虚拟线程,这就像在 JVM 内存中创建一个对象一样简单。
-
高性能:创建虚拟线程耗时更少(因为无需操作系统级别的操作),因此可以提升应用程序的整体性能。
-
更低的内存消耗:每个虚拟线程都在堆中维护一个栈,用于存储局部变量和方法调用。每个虚拟线程可以创建多个子线程,并且这些子线程的生命周期很短,因此每个线程的调用栈都很浅,内存消耗极低。
-
可扩展解决方案:API密集型应用程序通常采用每个请求一个线程的设计方式。由于虚拟线程允许JVM创建比平台线程更多的线程,因此应用程序可以扩展以服务大量客户端请求。
如何创建虚拟线程
当前的 JDK 框架支持两种创建虚拟线程的方法。以下是创建虚拟线程的示例代码。
方法一:使用 接口
ini
Thread.Builder builder = Thread.ofVirtual().name("NewThread");
Runnable task = () -> {
System.out.println("Running thread");
};
Thread t = builder.start(task);
System.out.println("Thread name: " + t.getName());
方法二: 框架
ini
try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<?> future = myExecutor.submit(() -> System.out.println("Running a new thread"));
future.get();
System.out.println("Task completed");
性能对比:虚拟线程 vs 平台线程
我创建了一个简单的程序来展示虚拟线程和平台线程之间的性能差异。
代码片段
vbnet
//Store 类用于存储由奇数和偶数生成的线程数。它使用并发哈希映射来存储这些数据。
public class Store {
private ConcurrentHashMap<Integer, Integer> concurrentHashMap = new ConcurrentHashMap<Integer, Integer>();
public synchronized void addQuantity(int productId){
int key = productId % 2;
concurrentHashMap.computeIfAbsent(key,k->0);
concurrentHashMap.computeIfPresent(key,(k,v)->v+1);
}
public Map<Integer, Integer> getStoreData(){
return concurrentHashMap;
}
}
java
//这个类充当一个任务,由多个线程执行。每个线程都有一个任务,即递增存储中的计数。
public class ComputationTask implements Runnable{
int productId;
Store store;
public ComputationTask(Store store, int id){
this.store = store;
productId=id;
}
@Override
public void run() {
store.addQuantity(productId);
}
ini
//这个主类包含实例化虚拟线程和平台线程的逻辑。它会创建超过 1000 个线程,所有线程完成工作后,代码会打印哈希映射条目以及整个进程所花费的总时间。
public class Main {
public static void main(String[] args) throws InterruptedException {
long pid = ProcessHandle.current().pid();
System.out.println("Process ID: " + pid);
Thread.Builder builder = null;
Store store = new Store();
builder = Thread.ofVirtual().name("virtual worker-", 0);
// builder = Thread.ofPlatform().name("platform worker-", 0);
long starttime = System.currentTimeMillis();
for (int i = 1; i < 1000; i++) {
ComputationTask task = new ComputationTask(store, i);
Thread t1 = builder.start(task);
t1.join();
System.out.println(t1.getName() + " started");
}
Map<Integer,Integer> map = store.getStoreData();
map.entrySet().stream()
.forEach(entry -> System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue()));
long endtime = System.currentTimeMillis();
System.out.println("Total Computation Time - "+(endtime-starttime)+" miliseconds");
}
}
执行结果
使用虚拟线程执行程序:
arduino
virtual worker-0 started
virtual worker-1 started
virtual worker-2 started
virtual worker-3 started
........................................
........................................
virtual worker-995 started
virtual worker-996 started
virtual worker-997 started
virtual worker-998 started
virtual worker-999 started
Key: 0, Value: 500
Key: 1, Value: 500
Total Computation Time - 147 miliseconds
Process finished with exit code 0
使用平台线程执行程序。
注释掉虚拟线程,在 Main.java 中创建代码,然后取消注释创建平台线程的代码。
vbnet
platform worker-0 started
platform worker-1 started
platform worker-2 started
platform worker-3 started
............................................
............................................
platform worker-995 started
platform worker-996 started
platform worker-997 started
platform worker-998 started
platform worker-999 started
Key: 0, Value: 500
Key: 1, Value: 500
Total Computation Time - 551 miliseconds
Process finished with exit code 0
根据上述结果可知,当程序创建 1000 个虚拟线程并执行特定操作时,耗时 147 毫秒;而相同的代码如果使用平台线程运行,则大约需要 551 毫秒才能完成。因此,虚拟线程比平台线程具有更好的性能。
使用虚拟线程的最佳实践
- 虚拟线程仅在执行同步代码块时才会绑定到平台线程。因此,应避免频繁且持续时间长的同步代码块,以缩短平台线程的生命周期,从而充分利用虚拟线程模型。
- 切勿创建虚拟线程池,因为虚拟线程数量庞大,创建新的虚拟线程开销很小。如果没有线程池,JVM 就不需要处理复杂的逻辑来维护线程池和进行调度。
- 避免使用异步代码编写技术,因为在同步代码中,服务器会为每个传入的请求分配一个线程,并持续处理整个请求周期。由于虚拟线程数量众多,阻塞它们的开销很小,因此值得鼓励。
- 虚拟线程支持线程局部变量。但是,由于虚拟线程数量可能很多,因此应谨慎使用线程局部变量。不要使用线程局部变量来在线程池中共享同一线程的多个任务之间分配昂贵的资源。