1相关概念
- 操作系统线程(operating system threads):即硬件设备配备的线程,一般和服务器硬件的核心数量,例如interCPU的大核拥有两个操作系统线程,小核拥有一个操作系统线程。云服务器的线程数一般和服务器配置上的核心数量相同。
- 平台线程(PaltformThread):java.lang.Thread 类的实例,是java对操作系统线程的一种包装,因此平台线程的数量和硬件线程数量一一对应。(the JDK implements threads as wrappers around operating system (OS) threads)
- 虚拟线程(Virtual Thread):java.lang.VirtualThread类的实例,是不依赖于特定操作系统线程的线程实例,由JVM管理,是一种轻量的线程。
- 载体线程: 运行虚拟线程的平台线程叫这个虚拟线程的载体线程。
2.线程之间的关系示意图
3.创建虚拟线程
创建虚拟线程的API常用的有四种。
1. 通过Thread类直接启动一个虚拟线程.
2. 通过 Thread.ofVirtual().unstarted方法创建一个虚拟线程,后续再通过代码调用start方法启动这个虚拟线程。
3. 通过Executors创建虚拟线程并直接运行提交的任务
4. 创建虚拟线程工厂,通过线程工厂创建一个未运行的虚拟线程。
通过打印的结果可以看出:四个虚拟线程分别启动完成。
打印的当前线程名称结构为: VirtualThread + 虚拟线程id + 任务运行的ForkJoinPool线程id(即平台线程id)
Runnable runnable = () -> {
System.out.println(STR."创建虚拟线程:\{Thread.currentThread()}");
};
//1.创建立刻启动
Thread.startVirtualThread(runnable);
//2.创建稍后启动
Thread unstartedVirtualThread = Thread.ofVirtual().unstarted(runnable);
unstartedVirtualThread.start();
//3.使用线程池的方法创建
Executors.newVirtualThreadPerTaskExecutor().submit(runnable);
//4.创建虚拟线程工厂 然后再用工厂创建虚拟线程 再启动虚拟线程
ThreadFactory threadFactory = Thread.ofVirtual().factory();
Thread FactoryVirtualThread = threadFactory.newThread(runnable);
FactoryVirtualThread.start();
通过打印的结果可以看出:四个虚拟线程分别启动完成。打印的当前线程名称结构为: VirtualThread + 虚拟线程id + 任务运行的ForkJoinPool线程id(即平台线程id)
4.通过代码测试虚拟线程和平台线程的关系
测试代码:同时启动两个虚拟线程,让他们分别打印当前的线程。使用线程休眠模拟虚拟线程阻塞的情况。
private static void testLogicalIndependence() {
long startTime = System.currentTimeMillis();
Thread.startVirtualThread(() -> {
while (System.currentTimeMillis() < startTime + 1000) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(STR."测试虚拟线程1:\{Thread.currentThread()}");
}
});
Thread.startVirtualThread(() -> {
while (System.currentTimeMillis() < startTime + 1000) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(STR."测试虚拟线程2:\{Thread.currentThread()}");
}
});
}
通过对比打印的结果:
可以看到虚拟线程1首先运行在平台线程work-3上,随后在虚拟线程阻塞的时候虚拟线程2运行在相同的平台线程work-3上。
1.证明了一个平台线程可以执行多个虚拟线程的任务。
当虚拟线程1再次启动并打印的时候,发现当前平台线程是work-2,
2.证明一个虚拟线程可以在多个平台线程上运行。
1 2可得:虚拟线程和逻辑线上在逻辑上是相互独立的。二者并不直接强关联。
5.测试虚拟线程和线程池之间的性能和吞吐量对比
测试代码:同时向虚拟线程和16线程的线程池提交一万个任务,对比他们任务执行结束的时间。使用Thread.sleep(1)模拟线程阻塞的情况。
long startTime = System.currentTimeMillis();
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
IntStream.range(0, 1_0_000).forEach(i -> executorService.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
}
)
);
executorService.close();
System.out.println(STR."执行的总时间:\{System.currentTimeMillis() - startTime}");
ExecutorService checkOutExecutor = new ThreadPoolExecutor(16, 16,
1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(Integer.MAX_VALUE / 2));
CompletionService<Integer> completionService = new ExecutorCompletionService<>(checkOutExecutor);
startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
completionService.submit(() -> {
try {
Thread.sleep(Duration.ofSeconds(1));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return 1;
});
}
for (int i = 0; i < 10000; i++) {
Future<Integer> future = completionService.poll(100000000, TimeUnit.MILLISECONDS); // 获取已完成的任务
}
System.out.println(STR."执行的总时间:\{System.currentTimeMillis() - startTime}");
checkOutExecutor.shutdown();
对比运行的时间可以得出:在多任务且线程频繁阻塞的情况下,虚拟线程的吞吐量要远大于线程池的吞吐量。
再将代码里面的Thread.sleep(1);注释掉重新运行
对比运行的时间可以得出:在线程不发生阻塞的情况下,使用虚拟线程的效率和使用线程池没有太大的区别。
综上可以得出虚拟线程使用的场景:
虚拟线程适用于执行阻塞式任务,在任务阻塞期间,将CPU资源让渡给其他任务使用
由于虚拟线程实际上依赖于操作系统线程,并没有增加任务执行的效率,所以虚拟线程不适用于CPU密集计算或者非阻塞类任务
6 虚拟线程调度
虚拟线程执行概念图
虚拟线程依赖于平台线程,jdk会将虚拟线程交给平台线程调度,然后操作系统线程和正常的代码一样操作平台线程。虚拟线程调度程序是一个ForkJoinPool以先进先出(FIFO)模式运行的工作窃取程序。
每个平台线程都可以运行多个虚拟线程,一般情况下,当某个虚拟线程(如图中虚拟线程4)被阻塞的时候,它会从当前上解除装载,当前的平台线程会空闲下来。如果此时其他平台线程有多余的虚拟线程任务需要执行,当前的平台线程会抢夺虚拟线程任务进行执行。之前阻塞的虚拟线程会在阻塞结束之后会挂载给随机的平台线程,等待平台线程执行。
7 关于虚拟线程的关注点
存在的意义:使代码语言的书写符合java的设计标准风格,即在一个线程中依次执行代码,使代码调试更加方便,代码运行前后的变量和堆栈信息可以在调试的时候展示在"同一个线程"中,而不是像使用异步编程时信息出现在多个线程中。在提高了硬件设备使用效率的前提下极大的减少了调试代码的难度。(和异步编程的功能类似,但是解决了异步编程难以调试的问题)
虚拟线程十分轻量,由JVM统一调度管理,不需要也不应该池化处理虚拟线程,用的时候创建结束的时候抛弃即可.
和平台线程不同的是,虚拟线程前后并不一定能够共享资源。例如在线程局部代码中创建一个数据库连接,并在稍后的代码中再次调用这个连接,这个操作在虚拟线程中是不安全的,可以使用缓存策略用来代替。
JDK中的绝大多数阻塞操作都会卸载虚拟线程,释放其载体和底层操作系统线程来承担新的工作。
但是当虚拟线程执行(synchronized块或方法内的代码时)或者(执行一个native方法或一个外部函数时)它是被固定在平台线程上的,这个时候是不能从平台线程上被卸载的。
尽量使用ReentrantLock代替sysnchronized 可以减少虚拟线程被固定的情况
虚拟线程始终是守护线程。且无法被修改成非守护线程。
虚拟线程的优先级固定为Thread.NORM_PRIORITY。在jdk21中无法被修改优先级
虚拟线程的堆栈作为堆栈块对象存储在 Java 的垃圾收集堆中。一般来说,虚拟线程所需要的堆空间和垃圾收集器的数量是比不上异步代码所需要的资源的。虚拟线程堆栈中的数据是不同于普通线程堆栈的,虚拟线程堆栈中的对象不直接跟随GCROOTS,所以可以被G1回收。