1、介绍
虚拟线程(Virtual Threads)和普通线程(Platform Threads)的基本概念。根据知识库,虚拟线程是JDK 21引入的,由JVM管理,而普通线程是操作系统级别的。需要解释它们的管理方式,比如虚拟线程在用户空间,而普通线程在内核空间。
虚拟线程更轻量,可以创建更多数量,适合高并发场景。而普通线程因为每个线程对应一个OS线程,资源消耗大,数量有限。特别是处理IO密集型任务时虚拟线程的优势。
2、比较
2.1 案例-1
@Test
public void test14(){
long start = System.currentTimeMillis();
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()){
IntStream.range(0, 1000000).forEach(i -> executorService.submit(() -> {
String aa = "xx";
}));
}catch (Exception e){
throw new RuntimeException(e);
}
long end = System.currentTimeMillis();
System.out.println("虚拟线程耗时:"+(end-start));
}
@Test
public void test15(){
long start = System.currentTimeMillis();
try (ExecutorService executorService = Executors.newFixedThreadPool(10)){
IntStream.range(0, 1000000).forEach(i -> executorService.submit(() -> {
String aa = "xx";
}));
}catch (Exception e){
throw new RuntimeException(e);
}
long end = System.currentTimeMillis();
System.out.println("普通耗时:"+(end-start));
}
结果:
(1)第一次执行
与预期一样
(2)第二次执行
与预期不一样
为什么会出现这种情况????
原因:
在运行上述代码时,有时会出现普通线程(newFixedThreadPool)执行比虚拟线程(newVirtualThreadPerTaskExecutor)快>的情况,这主要是由以下几个原因导致的:
(1) 虚拟线程的创建开销
虽然虚拟线程的创建和销毁成本相对较低,但在初始阶段,虚拟线程的创建过程仍然存在一定的开销。当任务数量较少或者任务本身非常简单时,创建虚拟线程的开销可能会成为性能瓶颈。在你的代码中,每个任务只是简单地创建一个字符串变量,任务本身的执行时间极短,此时虚拟线程的创建开销可能会对整体性能产生较大影响。
(2) 线程调度开销
虚拟线程是基于载体线程(通常是平台线程)进行调度的,线程调度需要一定的时间和资源。当任务简单且数量有限时,频繁的线程调度可能会导致性能下降。而newFixedThreadPool使用的是固定数量的平台线程,线程调度相对简单,开销较小。
(3)缓存局部性平台线程(普通线程)通常在硬件层面有更好的缓存局部性。当线程在执行任务时,会将数据加载到 CPU
缓存中,以便更快地访问。由于平台线程是直接与操作系统内核交互的,它们在执行过程中更有可能利用 CPU
缓存,从而提高性能。而虚拟线程在调度过程中可能会频繁切换载体线程,导致缓存局部性变差。
(4)测试环境的不确定性测试环境中的各种因素,如 CPU
负载、内存使用情况、操作系统的调度策略等,都会对测试结果产生影响。在不同的时间点运行测试代码,系统的状态可能会有所不同,从而导致测试结果的波动。
那我们又该如何使用才能体现虚拟线程的优势?
2.2 案例-2
@Test
public void test19() throws InterruptedException {
// 测试普通线程
testNormalThreads();
// 测试虚拟线程
testVirtualThreads();
}
private void testNormalThreads() throws InterruptedException {
long startTime = System.currentTimeMillis();
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
IntStream.range(0, REQUEST_COUNT).forEach(i -> {
executorService.submit(() -> {
try {
sendHttpRequest();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
});
});
executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
long endTime = System.currentTimeMillis();
System.out.println("普通线程执行 " + REQUEST_COUNT + " 个请求耗时: " + (endTime - startTime) + " 毫秒");
}
private void testVirtualThreads() throws InterruptedException {
long startTime = System.currentTimeMillis();
// 创建一个为每个任务创建一个虚拟线程的执行器
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, REQUEST_COUNT).forEach(i -> {
executorService.submit(() -> {
try {
sendHttpRequest();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
});
});
}
long endTime = System.currentTimeMillis();
System.out.println("虚拟线程执行 " + REQUEST_COUNT + " 个请求耗时: " + (endTime - startTime) + " 毫秒");
}
private void sendHttpRequest() throws IOException, InterruptedException {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(URL))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
}
结果:
(1) 第一次执行
(2)第二次执行
多次执行明显虚拟线程会比普通线程要执行快,性能好!