虚拟线程主要作用是提升服务器端的吞吐量。
吞吐量与下面3点有关:
比如一个服务器应用程序的延迟是50ms,处理10个并发请求,则吞吐量是200请求/秒(10 / 0.05)。如果吞吐量要提高到2000请求/秒,则处理的并发请求数量要提高到100。按照1个请求对应一个线程的比例来看,线程数量也要增加到100。
但java中的线程是在操作系统线程里进行了一层包装,而操作系统中线程与硬件相关,不能轻易增加,此时线程数量就限制了系统性能。当遇到以下场景时,系统很可能卡顿:
//ExecutorService实现了AutoCloseable接口,可以自动关闭了
try (ExecutorService executor = Executors.newCachedThreadPool()) {
//向executor中提交1000000个任务
IntStream.range(0, 1000000).forEach(
i -> {
executor.submit(() -> {
try {
//睡眠1秒,模拟耗时操作
Thread.sleep(Duration.ofSeconds(1));
System.out.println("执行任务:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
});
} catch (Exception e) {
e.printStackTrace();
}
以上代码中为每个任务创建一个线程,当任务量较多的时候,你的电脑可以感受到明显的卡顿(如果没有,可以增加任务数量试下)。
而如果将newCachedThreadPool改为newVirtualThreadPerTaskExecutor,就不会感到卡顿了。
以前线程和任务之间的关系:
使用虚拟线程之后,任务-虚拟线程-调度器-线程的关系,1个平台线程可以被调度器分配不同的虚拟线程:
调度器将虚拟线程挂载到线程之后,该线程叫做虚拟线程的携带器,在一个虚拟线程的生命周期中可以被分配到不同的携带器,即虚拟线程运行了一小段代码后,可能会脱离携带器,此时其他的虚拟线程会被分配到这个携带器上。
携带器和虚拟线程是相互独立的,比如:
在程序的执行过程中,虚拟线程遇到阻塞的操作时大部分情况下会被解除挂载,阻塞结束后,虚拟线程会被调度器重新挂载到携带器上,因此虚拟线程会频繁的挂载和解除挂载,这并不会导致操作系统线程的阻塞。譬如get方法和send方法(会有io操作)时会使虚拟线程发生挂载和解除挂载。
下面情况不会导致虚拟线程的解除挂载,会同时阻塞携带器和操作系统线程:
java中创建的虚拟线程本质都是通过Thread.Builder.OfVirtual对象进行创建的。
1.通过Thread.startVirtualThread直接创建一个虚拟线程
//创建任务
Runnable task = () -> {
System.out.println("执行任务");
};
//创建虚拟线程将任务task传入并启动
Thread.startVirtualThread(task);
//主线程睡眠,否则可能看不到控制台的打印
TimeUnit.SECONDS.sleep(1);
2.使用Thread.ofVirtual()方法创建
//创建任务
Runnable task = () -> {
System.out.println(Thread.currentThread().getName());
};
//创建虚拟线程命名为诺手,将任务task传入
Thread vt1 = Thread.ofVirtual().name("诺手").unstarted(task);
vt1.start();//启动虚拟线程
//主线程睡眠,否则可能看不到控制台的打印
TimeUnit.SECONDS.sleep(1);
也可以在创建虚拟线程的时候直接启动,
即将
Thread vt1 = Thread.ofVirtual().name("诺手").unstarted(task);
vt1.start();
换为
Thread vt1 = Thread.ofVirtual().name("诺手").start(task);
3.通过ExecutorService创建,为每个任务分配一个虚拟线程,下面代码中提交了100个任务,对应会有100个虚拟线程进行处理。
/*
通过ExecutorService创建虚拟线程
ExecutorService实现了AutoCloseable接口,可以自动关闭了
*/
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
//向executor中提交100个任务
IntStream.range(0, 100).forEach(i -> {
executor.submit(() -> {
//睡眠1秒
try {
Thread.sleep(Duration.ofSeconds(1));
System.out.println(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
});
}