Java 19 最核心的特性就是虚拟线程(Virtual Threads)
简介
该特性在Java19中是预览特性,虚拟线程是用户态下的线程,和go
语言中的goroutines
,Erlang
中的processes
类似,虚拟线程并非比线程快,而是提高了应用的吞吐量,相比于传统的线程是由操作系统调度来看,虚拟线程是我们自己程序调度的线程。虚拟线程的出现并没有修改Java原有的并发模型,也不会替代原有的线程,虚拟线程主要作用是提高服务器端的吞吐量(主要解决IO密集型而非CPU密集型任务)
吞吐量的瓶颈
服务器应用程序的伸缩性受利特尔法则(Little's Law)
的制约,与下面三点有关:
- 延迟:请求处理的耗时
- 并发量:同一时刻处理的请求数量
- 吞吐量:单位时间内处理的数据数量
如果一个服务处理延迟是50ms
,处理10个并发请求,则吞吐量是200请求/秒(10/0.05),如果吞吐量要达到2000请求/秒,则处理的并发请求数量是100. 如果按照1个请求一个线程来看,要想提高吞吐量,线程数量也要增加。
Java中的线程在操作系统线程(OS Thread)进行了一层封装,而操作系统重线程是重量级资源,在硬件配置确定的前提下,不能无限制创建线程。
与虚拟地址可以映射到物理内存类似,Java将大量的虚拟线程映射到少量的操作系统线程上,虚拟线程的生命周期短暂,不会有很深的栈调用,一个虚拟线程的生命周期只运行一个任务,因此可以大量创建虚拟线程,且无需池化。
虚拟线程的应用场景
在服务器端的应用程序中,虚拟线程能够明显提高应用的吞吐量:
- 至少几千的并发任务量
- 任务是IO密集型
平台线程和虚拟线程
平台线程(platform thread):指Java中的线程,比如通过new Thread()
创建的线程。
虚拟线程并不会直接分配给CPU执行,而是通过调度器
分配给平台线程,平台线程再被调度器管理。Java中的虚拟线程的调度器采用了工作窃取的模式进行FIFO的操作,调度器的并行书默认是jvm获取的处理器数量(Runtime.getRuntime().availableProcessors()
),调度器并非分时(time sharing
)的。在使用虚拟线程编写程序时,不能控制虚拟线程合适分配给平台线程,也不能控制平台线程合适分配给CPU。
以前任务和平台线程的关系:
在使用虚拟线程之后,任务-虚拟线程-调度器-平台线程的关系,1个平台线程可以被调度器分配不同的虚拟线程:
携带器
调度器将虚拟线程挂载到平台线程之后,该平台线程叫做虚拟线程的携带器,调度器并不维护虚拟线程和携带器的关联关系,因此在一个虚拟线程的生命周期可以被分配到不同的携带器,即虚拟线程运行了一小段代码后,可能会脱离携带器,此时其他的虚拟线程会被分配到这个携带器上。
携带器和虚拟线程是相互独立的,比如:
- 虚拟线程不能使用携带器的标识,
Thread.current()
方法获取的是虚拟线程本身。 - 两者有各自的栈空间。
- 两者不能访问对方的
ThreadLocal
变量
在程序的执行过程中,虚拟线程遇到阻塞的操作是大部分情况下会被解除挂载,阻塞结束后,虚拟线程会被调度器重新挂载到携带器上,因此虚拟线程会频繁的挂载和解除挂载,这并不会导致操作系统线程的阻塞。
有些阻塞操作并不会导致虚拟线程接触挂载,这样会同时阻塞携带器和操作系统线程,例如:操作系统基本的文件操作,Java的Object.wait()
方法,下面两种情况下不会导致虚拟线程的解除挂载:
- 执行
synchronized
同步代码(会导致携带器阻塞,所以建议使用ReentrantLock
替换掉Synchronized
) - 执行本地方法或外部函数
虚拟线程和平台线程api的区别
从内存空间上来说,虚拟线程的栈空间可以看做是一个大块的站对象,他被存储在Java堆中,相比于单独存储对象,堆中存储虚拟线程会造成一些空间的浪费,这点在后续的Java版本中应该会得到改善,当然也有一些好处,就是可以重复利用这部分栈空间,不用多次申请开辟新的内存地址,虚拟线程的栈空间最大可以达到平台线程的栈空间容量。
虚拟线程并不是GC root
,其中的引用不会出现Stop World
,当虚拟线程阻塞之后比如BlockingQueue.take()
,平台线程既不能获取到虚拟线程,也不能获取到queue
队列,这样该平台线程可能被回收掉,虚拟线程在运行或阻塞时不会被GC
- 通过Thread构造方法创建的线程都是平台线程
- 虚拟线程是守护线程,不能通过
setDaemon
方法改为非守护线程 - 虚拟线程的优先级是默认的5,不能被修改,将来版本可能允许修改
- 虚拟线程不支持
stop()
,suspend()
,resume()
方法
创建虚拟线程的方式
Java中创建的虚拟线程本质都是通过Thread.Builder.OfVirtual
对象进行创建的,有如下三种方式:
Thread.startVirtualThread()
直接创建一个虚拟线程Thread.ofVirtual().name("virtual-thread-").unstarted(r)
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()
示例代码
虚拟线程池
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
/**
* @author imyzt
* @date 2023/12/18
* @description VirtualThread 1
*/
public class VirtualThread {
public static void main(String[] args) throws InterruptedException {
// 通过线程池创建虚拟线程池
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10000000).forEach(i -> {
executor.submit(() -> {
try {
Thread.sleep(Duration.ofSeconds(1));
System.out.println("执行任务: " + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
创建虚拟线程
import java.util.concurrent.TimeUnit;
/**
* @author imyzt
* @date 2023/12/18
* @description VirtualThread 2
*/
public class CreateVirtualThread {
public static void main(String[] args) throws InterruptedException {
Runnable r = () -> System.out.println(Thread.currentThread().getName() + " --- 执行了");
// 创建虚拟线程, 方式1
Thread.startVirtualThread(r);
Thread virtualThread = Thread.ofVirtual().name("virtual-thread-").unstarted(r);
virtualThread.start();
System.out.println("是虚拟线程吗? " + virtualThread.isVirtual());
Thread platformThread = Thread.ofPlatform().priority(0).daemon(true).name("platform-thread-").unstarted(r);
platformThread.start();
System.out.println("是虚拟线程吗? " + platformThread.isVirtual());
// --- 执行了
//virtual-thread- --- 执行了
//是虚拟线程吗? true
//platform-thread- --- 执行了
//是虚拟线程吗? false
// 主线程休眠
TimeUnit.SECONDS.sleep(1);
}
}
其它
除了提出虚拟线程外,还提出来新的并发编程模型结构化并发,