在synchronized方法 + 虚拟线程 情况下会造成固定陷阱(Java虚拟线程不能使用同步synchronized锁!),为避免这种陷阱,JEP 491改变了JVM对synchronized关键字的实现,这个提案是虚拟线程提出后的进一步深化。
背景
虚拟线程是Java 21通过JEP 444引入的轻量级线程,由JDK而非操作系统提供。虚拟线程显著减少了开发、维护和观察高吞吐量并发应用程序的工作量,使应用程序能够使用大量线程。
虚拟线程的基本模型如下:
-
线程必须被_调度_才能做有用的工作,即被分配到处理器核心上执行。对于平台线程,JDK依赖于操作系统的调度器。对于虚拟线程,JDK有自己的调度器,它将虚拟线程分配给平台线程,然后由操作系统像往常一样调度。
-
虚拟线程在执行阻塞操作(如I/O)时会卸载,当阻塞操作准备完成时,操作将虚拟线程重新提交给JDK的调度器,调度器将虚拟线程重新安装到平台线程上以继续运行代码。
为何虚拟线程被固定到平台线程?
在Java中,虚拟线程被固定(pinned)到平台线程的原因主要与synchronized关键字的实现机制有关。
虚拟线程在synchronized方法内部运行代码时无法卸载。考虑以下synchronized从套接字读取字节的方法:
synchronized byte[] getData() {
byte[] buf = ...;
int nread = socket.getInputStream().read(buf); // Can block here ...
}
期望:
- 如果该read方法由于没有可用字节而阻塞,我们希望正在运行的虚拟线程getData从其载体上卸载。这将释放一个平台线程,以便 JDK 的调度程序可以在其上安装不同的虚拟线程。
实际:
- 不幸的是,由于getData是synchronized,JVM将正在运行的虚拟线程固定getData到其载体上。固定可防止虚拟线程卸载。
- 因此,该read方法不仅会阻塞虚拟线程,还会阻塞其载体,从而阻塞底层操作系统线程,直到有字节可供读取。
1、原因:
Java中的synchronized关键字是基于监视器(monitors)实现的,每个对象都与一个监视器关联,该监视器可以被获取(锁定)、持有一段时间,然后释放(解锁)。一次只能有一个线程持有对象的监视器。
在JVM中,哪个线程持有对象监视器的信息是基于平台线程(即操作系统线程)来跟踪的,而不是基于虚拟线程。当虚拟线程执行synchronized实例方法并获取与实例关联的监视器时,JVM记录的是虚拟线程的载体平台线程持有监视器,而不是虚拟线程本身。
如果虚拟线程在synchronized方法中卸载,JDK的调度器可能会将另一个虚拟线程安装到刚刚释放的平台线程上。
由于这个新虚拟线程的载体,JVM会认为它持有与实例关联的监视器,这将破坏互斥性,因此JVM积极阻止虚拟线程在synchronized方法中卸载。
如果虚拟线程在执行synchronized方法时调用了Object.wait(),它会在JVM中阻塞直到被Object.notify()唤醒,并且载体重新获取监视器。由于它在synchronized方法中执行,以及它的载体在JVM中被阻塞,所以虚拟线程也被固定pinned。
2、危害:
频繁的固定(尤其是长时间固定)可能会损害可扩展性,导致饥饿甚至死锁,因为没有虚拟线程可以运行,因为所有平台线程要么被虚拟线程固定,要么在JVM中被阻塞。
JEP 491目标
JEP 491旨在改变JVM对synchronized关键字的实现,允许虚拟线程在synchronized方法或语句中获取、持有和释放监视器,独立于它们的载体。这将允许虚拟线程在被阻塞时卸载,释放其载体平台线程给JDK调度器,从而提高应用程序的可扩展性
JEP 491具体解决了哪些技术问题?
通过允许在synchronized方法和语句中阻塞的虚拟线程释放其底层平台线程,供其他虚拟线程使用,从而提高Java代码在使用synchronized时的可扩展性。
解决了虚拟线程在synchronized方法中不能卸载的问题,这个问题限制了能够用于处理应用程序工作负载的虚拟线程数量。
同时,改进了诊断工具,以识别虚拟线程未能释放平台线程的情况,通过JDK Flight Recorder (JFR)记录jdk.VirtualThreadPinned事件来识别代码中的问题区域。
由于synchronized关键字不再固定虚拟线程,因此不再需要jdk.tracePinnedThreads系统属性,该属性会在虚拟线程在synchronized方法中阻塞时打印堆栈跟踪,但可能会引起性能问题。
解决了开发者在选择synchronized和java.util.concurrent.locks包中的API时的困惑,现在可以根据实际需要选择,而不必因为虚拟线程固定问题而被迫使用ReentrantLock。
JEP 491提议重新实现synchronized关键字,允许虚拟线程获取、持有和释放监视器,而不需要将这些操作与它们的载体线程绑定,解决了虚拟线程与监视器交互的问题。
本提案涉及对Object.wait()和Object.notify()机制的更改,允许虚拟线程在等待和重新获取监视器时挂起和恢复,而不会锁定其载体平台线程。
本提案识别了一些虚拟线程仍然会固定的情况,例如在类初始化器内阻塞、等待其他线程初始化类以及在类加载期间解析符号引用时阻塞。
JEP 491提出了替代方案
- 例如通过临时扩展虚拟线程调度器的并行性来补偿固定,但这种方法由于平台线程数量有限而不具可扩展性。
- 探讨了在JVM加载时重写每个类的字节码,用ReentrantLock替换synchronized的可能性,但这种方法会带来高开销和复杂性,并可能影响性能和与现有JVM特性的兼容性。