JDK 21 中的虚拟线程:革新 Java 多线程

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 就不需要处理复杂的逻辑来维护线程池和进行调度。
  • 避免使用异步代码编写技术,因为在同步代码中,服务器会为每个传入的请求分配一个线程,并持续处理整个请求周期。由于虚拟线程数量众多,阻塞它们的开销很小,因此值得鼓励。
  • 虚拟线程支持线程局部变量。但是,由于虚拟线程数量可能很多,因此应谨慎使用线程局部变量。不要使用线程局部变量来在线程池中共享同一线程的多个任务之间分配昂贵的资源。
相关推荐
悟能不能悟2 小时前
springboot controller返回的是HttpServletResponse成功返回excel文件流,失败就返回失败参数
spring boot·后端·excel
神奇小汤圆2 小时前
面试官:如何在 Kafka 中实现延迟消息?
后端
请告诉他2 小时前
【实战经验】Dell Inspiron 7560 升级 BIOS 支持 DDR4-2666 内存,解决 Spring Cloud 多模块开发内存瓶颈
后端·spring·spring cloud
我想问问天2 小时前
【从0到1大模型应用开发实战】02|用 LangChain 和本地大模型,完成第一次“可控对话
后端·langchain·aigc
爱吃牛肉的大老虎2 小时前
Spring WebFlux与SpringMVC 对比讲解
java·后端·spring
老华带你飞3 小时前
房屋租赁管理系统|基于java+ vue房屋租赁管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
superman超哥3 小时前
仓颉性能优化秘籍:内联函数的优化策略与深度实践
开发语言·后端·性能优化·内联函数·仓颉编程语言·仓颉·仓颉语言
IT_陈寒4 小时前
Vue3性能优化实战:7个被低估的Composition API技巧让渲染提速40%
前端·人工智能·后端