我对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万个平台线程并与它们一起工作,是仅仅因为上下文切换使得普通线程如此“重”吗?
一个非常相似的问题
到目前为止我发现:
- 上下文切换是昂贵的。一般来说,即使在操作系统知道线程行为的理想情况下,它仍然必须给予每个线程平等的执行机会,假设它们具有相同的优先级。如果我们产生10k个操作系统线程,操作系统将不得不在它们之间不断切换,而这个任务本身可能占用CPU时间的80%,所以我们必须非常小心地处理数字。对于虚拟线程,上下文切换由JVM完成,因此基本上是免费的。
- 廉价的启动/停止。当我们中断线程时,我们实际上告诉任务:“杀死你正在运行的操作系统线程”。但是,例如,如果该线程在线程池中,则当我们询问时,该线程可能已被当前任务释放,然后给另一个任务,并且其他任务可能会收到中断信号。这使得中断过程非常复杂。虚拟线程只是存在于堆中的对象,我们可以让GC在后台收集它们。
- 硬性上限(最多几万个)的线程,由于操作系统如何处理它们。操作系统无法针对特定应用程序和编程语言进行微调,因此必须为最坏情况下的内存做好准备。它必须分配更多的内存来容纳所有需求。在做所有这些事情的同时,它必须确保关键的操作系统进程仍在工作。使用VT,您只受到便宜的内存的限制。
- 执行事务的线程与执行视频处理的线程行为非常不同,再次,操作系统必须为最坏情况做好准备并尽力适应这两种情况,这意味着在大多数情况下我们会获得次优的性能。由于VT由Java自身产生和管理,因此允许完全控制它们和任务特定的优化,这些优化不受操作系统约束。
- 可调整大小的堆栈。操作系统为线程提供一个大堆栈以适应所有用例,虚拟线程具有可调整大小的堆栈,该堆栈位于堆空间中,根据问题动态调整大小使其更小。
- 较小的元数据大小。平台线程使用1MB,而虚拟线程需要200-300字节来存储它们的元数据。