当前位置:Java -> 驯服虚拟线程:拥抱并发编程并避免陷阱
在上一篇文章中,您学到了关于Java线程的历史以及Quarkus如何帮助开发人员在虚拟线程(Project Loom)上执行传统的阻塞应用,并且只需在方法和类级别使用单个@RunOnVirtualThread
注解就可以简单地实现。
不幸的是,在所有的Java生态系统中,虚拟线程还有很长的路要走。例如,您可能习惯于在Java项目中导入Maven依赖或Gradle模块。这意味着您可能不知道库是如何在平台线程上工作的,或者根本不需要知道,因为您只需要使用它们,这是常见的开发实践。在庞大的Java库生态系统中,您可能会遇到以下可能会破坏虚拟线程运行时的挑战和陷阱。
在Java虚拟线程中,固定指的是虚拟线程粘附在映射平台线程的载体线程上的情况。这意味着虚拟线程无法从载体线程中卸载,因为无法出于多种原因将虚拟线程的状态存储在堆内存中。
例如,当虚拟线程进入同步块或方法时,它会被固定到其载体线程,因为同步块和方法需要对共享资源进行独占访问,同一时间只允许一个线程进入同步块或方法,如图1所示。
图1:固定的载体线程
当您在虚拟线程上执行阻塞操作,如等待输入或输出时,它也会被固定到载体线程,因为虚拟线程在阻塞操作完成之前无法继续执行。当执行由本地代码执行的JNI调用时,也会发生固定,因为本地代码一次只能在一个线程上运行。
以下是一个固定代码的示例:
Object monitor = new Object();
//...
public void makePinTheCarrierThread() throws Exception {
synchronized(monitor) {
Thread.sleep(1000);
}
}
虚拟线程虽然提供了增强的并发性和性能潜力,但许多Java库目前会导致载体线程固定,从而可能影响应用程序的性能。Quarkus已经积极解决了这个问题,通过修补了一些库,如Narayana和Hibernate ORM,以消除固定。但是,随着开发人员采用虚拟线程,使用第三方库时仍需谨慎,因为并非所有代码都已经针对虚拟线程兼容性进行了优化。向虚拟线程友好的代码的渐进过渡正在进行中,随着生态系统的适应,其益处将不断积累。
当虚拟线程需要处理长时间的计算时,它会过度占用其载体线程,阻止其他虚拟线程利用该载体线程。例如,当虚拟线程反复执行阻塞操作,如等待输入或输出时,它会垄断载体线程,阻止其他虚拟线程取得进展。虚拟线程内部的资源管理不当也会导致过度资源利用,导致垄断载体线程。
垄断会对虚拟线程应用程序的性能和可扩展性产生不利影响。它可能导致对载体线程的争用增加,整体并发性降低,以及潜在的死锁。为了减轻这些问题,开发人员应努力通过将长时间的计算拆分成更小、更易管理的任务,以允许其他虚拟线程交错执行并利用载体线程来最小化垄断。
为了避免消耗昂贵的平台线程,发明了对象池来管理和重用频繁使用的对象以优化性能并减少内存开销。对象池还包括通过预先创建的对象池来为平台线程提供可用对象。例如,Threadlocal
就是一个很好的实现对象池的例子。然而,虚拟线程需要通过创建新实例而不是从对象池中检索新对象。此外,当虚拟线程完成时,对象被删除而不是返回到池中以供以后重用。
当您使用利用这些池化模式的Java库时,您的应用程序性能将会降低虚拟线程,因为您将看到许多大对象的分配,因为每个虚拟线程都将获得对象的实例。重新编写库以避免对象池模式应该是一项容易的工作。Quarkus还使用Jackson将昂贵的对象存储在线程局部变量中。幸运的是,已经有PR来解决这个问题,使Quarkus应用程序更加友好于虚拟线程。
您了解了当您尝试在虚拟线程上运行微服务时可能面临的挑战。如果需要导入更多的Java库来实现各种业务领域,如REST客户端、消息传递、gRPC、事件总线等,挑战可能会更加复杂和更大。这告诉我们,在所有业务服务上使用虚拟线程还有很长的路要走。
与此同时,Quarkus已经对一些现有的Quarkus扩展,例如Apache Kafka、AMQP、Apache Pulsar、JMS、REST客户端、Redis、Mailer、事件总线、定时方法、gRPC等,进行了补丁和重写,以适应虚拟线程。在下一篇文章中,我将介绍如何对虚拟线程进行富有洞察力的性能比较,以及虚拟线程与阻塞和响应式应用程序的优劣对比。
推荐阅读: 程序员在南京能进什么公司
本文链接: 驯服虚拟线程:拥抱并发编程并避免陷阱