背景
我们在Java中经常使用的线程叫做平台线程(Platform Threads)它与操作系统内核空间中的线程对应,它由操作系统进行管理,可创建最大数量受限于操作系统。
在Linux中可以通过
cat /proc/sys/kernel/threads-max
命令查看操作系统单个进程支持最大线程数量。
在高并发应用中,我们常使用多线程来提高程序处理效率,当线程创建数量达到瓶颈便无法处理更多的请求。线程的管理开销较高,需要CPU频繁切换时间片,而我们池化后也只能解决线程创建和销毁的开销,并不能创建更多的线程数量,也无法解决频繁切换线程开销的问题。
什么是虚拟线程?
虚拟线程(Virtual Threads)也就是其他语言中的协程。例如:Python中的coroutine,GoLang中的goroutine。
为什么要使用虚拟线程?
虚拟线程是以平台线程为载体的轻量级线程,它运行在用户空间内由JVM管理,占用空间更小,同时也能减少线程切换的开销,支持创建更多的虚拟线程,支持更多的并发量,提高性能。可以更有效的利用资源,降低运营成本。
如何创建一个虚拟线程?
方式一
public class Main {
public static void main(String[] args) {
// 创建一个平台线程
Thread.ofPlatform().start(()->{
System.out.println("pt="+Thread.currentThread());
});
// 创建一个虚拟线程
Thread vt = Thread.ofVirtual().start(() -> {
System.out.println("vt="+Thread.currentThread());
});
// 等待虚拟线程执行完毕,再退出主程序
vt.join();
//或使用效果一样 new CountDownLatch(1).await(2, TimeUnit.SECONDS);
}
}
运行结果:
pt=Thread[#21,Thread-0,5,main]
vt=VirtualThread[#22]/runnable@ForkJoinPool-1-worker-1
vt2=VirtualThread[#24]/runnable@ForkJoinPool-1-worker-2
其中虚拟线程 VirtualThread[#22]
运行在平台线程worker-1
中,worker-1
是ForkJoinPool-1
线程池中的线程。
方式二:通过线程池的方式创建虚拟线程
@Test
public void testVtExecutor() throws InterruptedException {
var max = 1_000_000;
// 创建一个计时器
CountDownLatch latch = new CountDownLatch(max);
// 统计平台线程
Set<String> ptNames = ConcurrentHashMap.newKeySet();
// 虚拟线程池
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 创建100万个虚拟线程
IntStream.range(0, max).forEach(i ->{
executor.submit(() -> {
// 计时器减1
latch.countDown();
// 每个虚拟线程 睡眠1秒并统计平台线程名称
Thread.sleep(Duration.ofSeconds(1));
ptNames.add(Thread.currentThread().toString().split("@")[1]);// VirtualThread[#9297]/runnable@ForkJoinPool-1-worker-12
return i;// callable抛出异常
});
});
// 等待统计结果
latch.await();
// 打印
System.out.println("ptNames.size=" + ptNames.size());
}
运行结果:
ptNames.size=12
可以看到100万个虚拟线程只用了12个平台线程,极大减少了线程创建数量。
虚拟线程原理
一个平台线程对应多个虚拟线程,但不是一一绑定的,每个平台线程会维护一个任务列表,并从此列表获取虚拟线程。
- 当虚拟线程启动时,它会绑定到某个平台线程,平台线程作为载体运行在线程池中。
- 当虚拟线程阻塞时,它会从平台线程中卸载。这个平台线程就可以运行其他虚拟线程。甚至去其他平台线程中获取任务。
- 当虚拟线程恢复到非阻塞时,它会加载到某个平台线程的任务列表中。
这样我们就可以利用少量的平台线程,来运行大量的虚拟线程,从而节省平台线程资源。
虚拟线程的使用场景
- 虚拟线程更适合处理阻塞式任务,在阻塞期间将CPU资源让给其他任务来提高效率。
- 不适合执行CPU密集型或非阻塞任务,虚拟线程不会运行的更快,只会提高规模。