我为Project Loom大肆宣传,但有一件事我不能完全理解。
大多数 Java 服务器使用具有一定线程限制(200、300 ..)的线程池,但是,您不受操作系统的限制来生成更多线程,我已经读到,使用 Linux 的特殊配置,您可以达到巨大的数量。
操作系统线程更昂贵,启动/停止速度较慢,必须处理上下文切换(按其数量放大),并且您依赖于操作系统,操作系统可能会拒绝为您提供更多线程。
话虽如此,虚拟线程也会消耗相似数量的内存(或者至少这是我所理解的)。使用Loom,我们可以获得尾部调用优化,这应该可以减少内存使用。此外,同步和线程上下文复制应该仍然是类似大小的问题。
事实上,您能够产生数百万个虚拟线程
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
Thread.startVirtualThread(() -> {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
上面的代码在 25k 左右中断,当我使用平台线程时出现 OOM 异常。
我的问题是,究竟是什么让这些线程如此轻巧,是什么阻止我们生成 100 万个平台线程并使用它们,只是上下文切换使常规线程如此“繁重”。
一个非常相似的问题
到目前为止我发现的东西:
协程(即虚拟线程)的一大优点是它们可以生成高级别的并发性,而没有回调的缺点。
我先介绍一下利特尔定律:
concurrency = arrival_rate * latency
我们可以将其重写为:
arrival_rate = concurrency/latency
在稳定的系统中,到达率等于吞吐量。
throughput = concurrency/latency
要提高吞吐量,您有两个选项:
对于常规线程,由于上下文切换开销,很难通过阻塞调用达到高并发级别。在某些情况下,请求可以异步发出(例如,NIO Epoll或Netty io _ uring绑定),但这时您需要处理回调和回调地狱。
使用虚拟线程,可以异步发出请求并停放虚拟线程并调度另一个虚拟线程。一旦收到响应,虚拟线程就会被重新调度,这是完全透明的。编程模型比使用经典线程和回调直观得多。
虚拟线程被包裹在平台线程上,所以你可能会认为它们是JVM提供的一种幻觉,整个想法是将线程的生命周期变成CPU绑定的操作。
究竟是什么让Java虚拟线程变得更好?
虚拟线程优势
虚拟线程使用注意事项
> < li>
不要使用monitor,即synchronized块,但是这将在JDK的新版本中修复,替代方法是使用带有try-final语句的“ReentrantLock”。
堆栈上的本机帧阻塞,JNI's。这非常罕见
控制每个堆栈的内存(减少线程区域设置,无深度递归)
尚未更新的监控工具,如调试器、JConsole、VisualVM等
平台线程与虚拟线程。平台线程在基于 IO 的任务和操作中将操作系统线程作为人质,仅限于线程池和操作系统线程中的适用线程数,默认情况下它们是非守护程序线程
虚拟线程是用JVM实现的,在CPU绑定操作中,关联到平台线程并将它们返回到线程池,在IO绑定操作完成后,将从线程池中调用一个新线程,因此在这种情况下没有人质。
第四级架构有更好的理解。
中央处理器
操作系统
JVM
带有执行器服务的虚拟线程
> 使用executer服务更有效,因为它与线程池相关联,并且仅限于与它相关联的可用线程,但是与虚拟线程相比,使用Executer服务和虚拟容器,我们不需要处理或管理相关联的线程池。
try(ExecutorService service = Executors.newVirtualThreadPerTaskExecutor()) {
service.submit(ExecutorServiceVirtualThread::taskOne);
service.submit(ExecutorServiceVirtualThread::taskTwo);
}
Executor服务在JDK 19中实现了Auto Closable接口,因此当与“try with resource”一起使用时,一旦到达“try”块的末尾,就会调用“close”api,或者主线程将等待所有提交的任务及其专用虚拟线程完成其生命周期,并关闭相关的线程池。
ThreadFactory factory = Thread.ofVirtual().name("user thread-", 0).factory();
try(ExecutorService service = Executors.newThreadPerTaskExecutor(factory)) {
service.submit(ExecutorServiceThreadFactory::taskOne);
service.submit(ExecutorServiceThreadFactory::taskTwo);
}
执行器服务也可以使用虚拟线程工厂创建,只需将线程工厂与其构造函数参数一起放置即可。
可以受益于未来和完整未来等执行器服务的功能。
在JEP-425上查找更多信息
有时,人们必须构建能够处理大量并发客户端的系统。由于RAM消耗和上下文切换成本,本机线程不足以实现这一点。
虚拟线程让我们能够同时运行数百万个I/O任务,而无需改变我们的思维模式。
这就是Golang进入这个行业的原因(除了谷歌的支持)。Goroutines是一个与Java的虚拟线程非常相似的概念,它们解决了同样的问题。
还有其他方法可以实现虚拟线程的功能(例如NIO和相关的React模式)。然而,这需要使用扭曲你思维的消息循环和回调(这就是为什么这么多人讨厌JavaScript)。在它们之上有抽象层,让事情变得更容易,但它们也有成本。