JEP No 425 是我期待已久的东西。这是 JDK 19 的并发 API 中添加的一个新概念。它处于预览阶段,很快它将在未来的几个版本中成为 JDK 中的永久功能。
虚拟线程非常轻量级,可以减少编写、维护和观察高吞吐量应用程序的工作量。
在这一部分中,我们将认识并理解这个 JEP 的目标。
第一个是使以简单的每个请求线程风格编写的服务器应用程序能够以接近最佳的硬件利用率进行扩展。
让我们了解"每请求每线程"模型。嗯,对于 HTTP 服务器来说,这意味着每个 HTTP 请求都由它自己的线程处理。对于关系数据库服务器来说,这意味着每个SQL事务也由它自己的线程处理。很简单的模型。
One request = one transaction = one thread.
这个模型的成本是多少?好吧,为了理解这个成本,我们需要了解Java中线程的成本。在 Java 的早期版本中创建的 Java 线程是平台线程(也称为操作系统线程)上的简单包装器。
现在,一些数学方面的事情正在进行中。随身携带一个计算器。
关于它们,我们需要了解两件事。首先,平台线程需要将其调用栈存储在内存中。为此,预先在内存中预留了 20MB。其次,它是系统资源。启动平台线程大约需要 1 毫秒。因此,20MB 内存,启动需要 1 毫秒。平台线程实际上是一种相当昂贵的资源。
我们如何利用这些线程来优化硬件利用率?
假设我们有 16GB 的内存可供应用程序使用。除以一个线程的 20MB,在这样的机器上有 800 个线程 (16 * 1000 / 20) 的空间。假设这些线程正在执行一些 I/O,例如访问网络上的资源。并假设该资源在 100 毫秒内被访问。准备请求和处理响应将分别以 500 纳秒的顺序完成(如上所示)。假设所有这些内存计算需要 1000 纳秒。现在从图中可以看出,由于 1ms = 1000,000 纳秒,这意味着在请求的准备和响应的处理之间存在一个 100,000 量级的因素。在此期间,我们的线程空闲,什么也不做。花费数千美元购买 CPU,使其闲置。真可惜?
因此,如果我们有 800 个这样的线程,我们的 CPU 利用率将为 0.8%。小于1%。如果我们将内存加倍到 32GB,则使用率将达到 1.3%。如果我们想要 90% 的 CPU 使用率,那么我们需要 90,000 个这样的线程。启动它们需要 90 秒,即 1 分半钟,并且将消耗 1.8 TB 内存。如果市场上有这样的内存,我很确定只有少数人买得起。显然,平台线程的成本太高,无法通过接近最佳的硬件利用率进行扩展。 Loom 项目正在解决这个问题。
第二个目标是使基于经典 Java 线程的现有代码能够以最小的更改采用虚拟线程。这个目标也相当雄心勃勃,因为这意味着您可以用经典线程做的所有事情,您应该能够以与虚拟线程相同的方式完成。这涵盖了几个关键点:
首先,虚拟线程可以运行任何Java代码或任何本机代码。
其次,你不需要学习任何新概念。
第三,但你需要忘掉某些想法。虚拟线程很便宜,比传统平台线程便宜大约 1000 倍,因此试图避免阻塞虚拟线程是没有用的。编写经典的阻塞代码是可以的。这是一个好消息,因为阻塞代码比异步代码更容易编写。
池化虚拟线程是个好主意吗?答案是一个很大的禁忌,因为它很便宜,不需要将它们集中起来,只需要按需创建和销毁。
关于虚拟线程的另外两个好消息首先,线程局部变量也以同样的方式工作。其次,同步也有效。关于同步,现在需要说几件事。虚拟线程仍然运行在平台线程之上。下面还有一个平台线。诀窍是,这个虚拟线程可以与其平台线程分离,以便该平台线程可以运行另一个虚拟线程。什么时候才能脱离(平台线程)呢?好吧,虚拟线程一旦阻塞就可以与其平台线程分离。它可能会在 I/O 操作、同步操作或进入睡眠状态时被阻止。
需要注意的是,如果虚拟线程正在同步块内执行某些代码,则它无法与其平台线程分离。因此,在运行同步代码块期间,它会阻塞平台线程。如果这个时间很短也没关系,不必惊慌。如果这个时间很长,也就是说,如果它正在做一些长时间的I/O操作,那么情况就不太妙了,我们可能需要做点什么。我们需要用可重入锁 API 替换同步块。同步块的这个问题将来可能会得到解决,事实上,当虚拟线程成为 JDK 的最终功能时,它可能会得到解决。
市面上也有响应式编程,也在尝试解决这个问题。但反应式的问题在于,它是我们编码方式的彻底范式转变。它很难学习,很难理解,很难分析代码,更难以调试,并且编写测试用例是一场噩梦。
希望您已经了解 Loom 项目试图解决的问题。在接下来的部分中,我们将研究一些编码。我对这个项目感到很兴奋。