Java 通过 JEP 491 应对虚拟线程固定问题

JEP 491:在虚拟线程中同步而不固定

JEP 491(Synchronize Virtual Threads without Pinning)已被提升为JDK 24的Proposed to Target状态。该提案旨在彻底改革Java的synchronized方法与虚拟线程的交互方式,以消除长期以来限制线程可扩展性的“固定”问题。虚拟线程在JDK 21中引入,能够显著提升应用程序的吞吐量,支持数十万个线程。然而,synchronized关键字导致的固定问题限制了开发者充分利用这一创新。JEP 491解决了这一问题,为高性能、高并发的应用程序铺平了道路,且无需进行大量重构。

问题背景

在Java中,同步是通过与每个对象关联的监视器实现的。线程进入synchronized块或方法时,会获取监视器的锁,确保同一时间只有一个线程可以执行该块或方法。然而,在当前Java模型中,JVM将监视器锁与平台线程关联,而不是虚拟线程。这种关联导致了固定问题:一旦虚拟线程获取锁,它就会被绑定到底层平台线程,阻止调度器将其重新分配给其他虚拟线程。

例如,以下代码展示了从套接字读取字节的方法:

synchronized byte[] getData() {
    byte[] buf = ...;
    int nread = socket.getInputStream().read(buf); // 可能在此处阻塞
    ...
}

如果该方法在读取操作期间阻塞,JVM会将虚拟线程固定到其平台线程,消耗资源直到数据可用。这种阻塞限制了可扩展性,因为固定的虚拟线程占用了平台线程,而这些线程本可以支持更多的虚拟线程。

JEP 491的解决方案

JEP 491提出允许虚拟线程在同步块内卸载,释放其平台载体线程以执行其他虚拟线程。这将涉及对Object.wait()Object.notify()机制的更改,使虚拟线程能够在同步块内挂起和恢复,而不会锁定其平台载体线程。

例如,以下代码展示了虚拟线程在同步块内调用Object.wait()时的行为:

synchronized void waitForCondition() {
    while (!condition) {
        wait();  // 虚拟线程在此处卸载
    }
    ...
}

通过这种方式,应用程序可以更好地扩展,而无需开发者放弃synchronized转而使用java.util.concurrent锁。

开发工具与兼容性

为了帮助开发者识别问题代码,JDK目前在虚拟线程在同步方法内阻塞时,在JDK Flight Recorder (JFR)中记录jdk.VirtualThreadPinned事件。该事件对于定位需要重构代码或从synchronized切换到java.util.concurrent锁的场景非常有价值。随着JEP 491的更改,jdk.VirtualThreadPinned事件将仅在虚拟线程与本地代码交互的场景中保持相关。此外,jdk.tracePinnedThreads系统属性将被移除,以避免性能问题。

特殊情况与替代方案

JEP 491承认,某些特殊情况仍会导致虚拟线程固定,例如类初始化期间的阻塞、等待其他线程完成类初始化,以及在类加载期间解析符号引用时的阻塞。虽然这些情况相对罕见,但在高并发应用程序中可能会引发问题。提案建议监控这些场景,并在未来更新中重新审视它们。

替代方案包括增加虚拟线程调度器的并行性以减少固定的影响,但这种方法受限于调度器的默认256个平台线程上限。另一个选项是动态重写字节码,将synchronized替换为ReentrantLock,但这种方法会带来高开销和复杂性,可能影响性能和与现有JVM功能的兼容性。

总结

JEP 491是Java虚拟线程和并发模型演进的关键一步。通过允许虚拟线程在同步块内卸载,开发者可以构建高吞吐量的应用程序,而无需进行之前的权衡。该提案不仅简化了并发编程,还提升了Java在高并发场景下的表现。

阅读 170
0 条评论