Jetty 12推出虚拟线程支持


在Java 19中引入的虚拟线程在Jetty 12中已经得到了支持。

当虚拟线程被JVM支持并在Jetty中启用时(见嵌入式使用和独立使用),应用程序会被使用虚拟线程调用,这使得它们可以使用简单的阻塞API,但具有虚拟线程的可扩展性优势。

虚拟线程在Java 19中通过JEP 425作为预览功能引入,在Java 20中通过JEP 436引入,最后在Java 21中通过JEP 444整合为正式功能。

历史上,提供给应用开发者,特别是网络应用开发者的API是基于InputStream和OutputStream的阻塞式API,或基于JDBC的API。
这些API使用起来非常简单,所以应用程序的开发、理解和故障排除都很简单。

然而,这些API是有代价的:当一个线程阻塞时,通常是等待I/O或争夺锁,所有与该线程相关的资源都会被保留,等待线程解除阻塞并继续处理:本地线程及其本地内存被保留,还有网络缓冲区、锁结构等。

这意味着阻塞式API的可扩展性较差,因为它们会保留所使用的资源。
例如,如果你的服务器线程池配置了256个线程,而所有的线程都被阻塞了,你的服务器就不能处理其他的请求,直到其中一个被阻塞的线程解除阻塞,从而限制了服务器的可扩展性。

此外,如果你增加服务器线程池的容量,你将使用更多的内存,并可能需要更大的硬件。

异步/反应式
由于这些原因,非阻塞的异步和反应式API已经被引入。主要的例子是Servlet 3.1中引入的异步I/O API,以及由RxJava和Spring的Project Reactor等库提供的基于Reactive Streams的反应性API。
不幸的是,JAX-RS或Jakarta RESTful Web服务等REST API还没有(完全)更新为非阻塞式API,因此使用REST的Web应用被阻塞式API和扩展性问题所困。

需要注意的是,异步和反应式API比阻塞式API更难使用、理解和排除故障,但其可扩展性更强,通常只需花费一小部分资源就能达到类似的性能。我们看到,当从阻塞式API切换到非阻塞式API时,Web应用程序的线程用量从1000多个减少到10多个。

虚拟线程的目标是成为两个世界中最好的:

  • 为开发者提供简单易用的阻塞式API,
  • 并由JVM提供非阻塞式API的可扩展性。

Jetty 12的架构
Jetty 12架构的核心是完全无阻塞的,并使用AdaptiveExecutionStrategy(以前称为 "吃你所杀",在以前的博客中曾介绍过)来决定如何消费任务。

AdaptiveExecutionStrategy的关键特征是,它非常倾向于在产生任务的同一线程中消耗任务,因此它们在执行时有一个热的CPU缓存,没有并行减速,也没有上下文切换延迟,但又避免了服务器耗尽其线程池的风险。

简化一下,每个任务都被标记为阻塞或非阻塞;AdaptiveExecutionStrategy查看任务和有多少线程可用,以决定如何消耗该任务。

如果任务是非阻塞的,当前线程会立即运行它。
否则,如果没有其他线程可以继续生产任务,当前线程就会接管任务的生产,并将任务交给一个Executor,在那里它们可能会被排队,并在以后由不同的线程执行。

虚拟线程集成
这种架构使得在Jetty中集成虚拟线程变得很容易:当JVM支持虚拟线程并且Jetty的虚拟线程支持被启用时(见嵌入式用法和独立式用法),AdaptiveExecutionStrategy通过向虚拟线程执行器而不是本地线程执行器提供任务来消耗一个阻塞任务,这样一个新产生的虚拟线程就会运行这个阻塞任务。

作为一个Servlet容器的实现,Jetty调用Servlet时假定它们会使用阻塞式API,所以调用Servlet的任务是一个阻塞式任务。
当虚拟线程被支持和启用时,调用Servlet过滤器并最终调用HttpServlet.service(...)方法的线程就是一个虚拟线程。

对于非阻塞任务,让它们由创建它们的同一个本地线程来运行会更有效率;只有对于阻塞任务,你才可能想要使用虚拟线程。

总结
Jetty的自适应执行策略(AdaptiveExecutionStrategy)实现了所有世界中最好的虚拟线程调度策略。

Jetty提供了一个快速可扩展的异步实现,它避免了虚拟线程的任何可能的限制,同时给应用程序提供了虚拟线程的全部好处。