👏作者简介:大家好,我是若明天不见,BAT的Java高级开发工程师,CSDN博客专家,后端领域优质创作者
📕系列专栏:多线程及高并发系列
📕其他专栏:微服务框架系列、MySQL系列、Redis系列、Leetcode算法系列、GraphQL系列
📜如果感觉博主的文章还不错的话,请👍点赞收藏关注👍支持一下博主哦??
?时间是条环形跑道,万物终将归零,亦得以圆全完美
多线程及高并发系列
虚拟线程是 Project Loom 的核心概念,它是一种轻量级的线程实现,具有以下关键特点:协作式调度、轻量级堆栈、阻塞不占用线程、与现有代码兼容。虚拟线程的引入旨在提高并发性能和可伸缩性,简化并发编程,并最大程度地利用系统资源
Fibers API
提供了一种新的编程模型,可以在现有的 Java 应用程序中使用,而无需更改现有的线程管理代码虚拟线程(Virtual Thread)它不与特定的操作系统线程相绑定。它在平台线程上运行 Java 代码,但在代码的整个生命周期内不独占平台线程。**这意味着许多虚拟线程可以在同一个平台线程上运行他们的 Java 代码,共享同一个平台线程。详见虚拟线程原理及性能分析
此段自虚拟线程原理及性能分析
虚拟线程是由 Java 虚拟机调度,而不是操作系统。虚拟线程占用空间小,同时使用轻量级的任务队列来调度虚拟线程,避免了线程间基于内核的上下文切换开销,因此可以极大量地创建和使用
简单来看,虚拟线程实现如下:virtual thread = Continuation + Scheduler + Runnable
Continuation
实例中:
Continuation.yield()
操作进行阻塞,虚拟线程会从平台线程卸载Continuation.run()
会从阻塞点继续执行java.util.concurrent.Executor
的子类FIFO
的ForkJoinPool
用于执行虚拟线程任务JVM 把虚拟线程分配给平台线程的操作称为 mount(挂载),取消分配平台线程的操作称为 unmount(卸载):
从 Java 代码的角度来看,其实是看不到虚拟线程及载体线程共享操作系统线程的,会认为虚拟线程及其载体都在同一个线程上运行,因此,在同一虚拟线程上多次调用的代码可能会在每次调用时挂载的载体线程都不一样
spring.threads.virtual.enabled: true
在 Spring Boot 3.2 中启用虚拟线程,只需在application.yml
或application.properties
文件中设置这个属性
@Async
方法时,Spring MVC 的异步请求处理和 Spring WebFlux 的阻塞执行支持现在将使用虚拟线程@Scheduled
的方法将在虚拟线程上运行JDK 的虚拟线程调度器是一个工作窃取 ForkJoinPool,以先进先出(FIFO )模式运行。调度器的并行性(parallelism)是指可用来调度虚拟线程的平台线程数。
ForkJoinPool的原理及核心概念可见 parallelStream/ForkJoinPool 详解
方法一:直接创建虚拟线程
Thread vt = Thread.startVirtualThread(() -> {
System.out.println("hello wolrd virtual thread");
});
方法二:创建虚拟线程但不自动运行,手动调用start()开始运行
Thread.ofVirtual().unstarted(() -> {
System.out.println("hello wolrd virtual thread");
});
vt.start();
方法三:通过虚拟线程的 ThreadFactory 创建虚拟线程
ThreadFactory tf = Thread.ofVirtual().factory();
Thread vt = tf.newThread(() -> {
System.out.println("Start virtual thread...");
Thread.sleep(1000);
System.out.println("End virtual thread. ");
});
vt.start();
方法四:Executors.newVirtualThreadPer -TaskExecutor()
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {
System.out.println("Start virtual thread...");
Thread.sleep(1000);
System.out.println("End virtual thread.");
return true;
});
了解了虚拟线程的原理及使用,准备将服务从JDK 17 升级到 JDK 21,准备用虚拟线程尝尝鲜。然而一压测发现,性能竟然急剧下降l了?!
加上JVM 参数jdk.tracePinnedThreads
当线程在block
下发生阻塞时会触发堆栈跟踪
full
为打印完整的堆栈跟踪,突出显示 native 堆栈帧和持有监视器的堆栈帧short
为输出限制为仅出现问题的堆栈帧-Djdk.tracePinnedThreads=full
当线程在固定状态下发生阻塞时,会发出
JDK Flight Recorder (JFR)
事件(请参阅Java Flight Record 详解)
block 线程详细堆栈:
Thread[#57,ForkJoinPool-1-worker-2,5,CarrierThreads]
java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:185)
java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
java.base/java.lang.VirtualThread.park(VirtualThread.java:592)
java.base/java.lang.System$2.parkVirtualThread(System.java:2639)
java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:54)
java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:369)
java.base/sun.nio.ch.Poller$Request.awaitFinish(Poller.java:215)
java.base/sun.nio.ch.Poller.pollIndirect(Poller.java:143)
java.base/sun.nio.ch.Poller.poll(Poller.java:102)
java.base/sun.nio.ch.Poller.poll(Poller.java:89)
java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:175)
java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:548)
java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:592)
java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327)
java.base/java.net.Socket.connect(Socket.java:751)
java.base/sun.security.ssl.SSLSocketImpl.connect(SSLSocketImpl.java:304)
java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
java.base/java.lang.reflect.Method.invoke(Method.java:580)
org.apache.commons.httpclient.protocol.ReflectionSocketFactory.createSocket(ReflectionSocketFactory.java:140)
org.apache.commons.httpclient.protocol.SSLProtocolSocketFactory.createSocket(SSLProtocolSocketFactory.java:130)
org.apache.commons.httpclient.HttpConnection.open(HttpConnection.java:707)
org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$HttpConnectionAdapter.open(MultiThreadedHttpConnectionManager.java:1361)
org.apache.commons.httpclient.HttpMethodDirector.executeWithRetry(HttpMethodDirector.java:387)
org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:171)
org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397)
org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:323)
net.oauth.client.httpclient3.HttpClient3.executeOnce(HttpClient3.java:149)
net.oauth.client.httpclient3.HttpClient3.execute(HttpClient3.java:90)
net.oauth.client.OAuthClient.access(OAuthClient.java:325)
net.oauth.client.OAuthClient.invoke(OAuthClient.java:306)
net.oauth.client.OAuthClient.invoke(OAuthClient.java:262)
com.test.auth.TestSync.test(TestSync.java:120) <== monitors:1
TestSync
是依赖其他业务的 jar 包,堆栈中的monitors:1可见方法获取了监视器锁,即进入了synchronized
方法。该synchronized
方法里执行了I/O操作。
见虚拟线程官方文档,在两种情况下,虚拟线程在阻塞操作期间无法卸载,因为它绑定了载体线程:
synchronized
块 或 synchronized
方法内执行代码native
方法 或 外部函数也就是说,TestSync 类中 test 方法是synchronized
方法,在方法里执行了较耗时的 I/O 操作,导致 JVM 无法把虚拟线程从平台线程中卸载,使得虚拟线程及其载体线程都被阻塞了,最终导致 Tomcat 容器中无平台线程来执行其他操作
synchronized test {
// I/O操作
}
那究竟是为什么 Project Loom 中,synchronized
为什么会导致虚拟线程不能卸载,从而导致虚拟线程及其载体线程都被阻塞了呢?
在当前的Java平台中,synchronized 关键字在底层是通过 Java 对象监视器实现的,其使用的是操作系统线程(OS Thread)。而在 Project Loom 中,为了实现轻量级的并发操作,引入了虚拟线程(
virtual threads
)的概念,也称为纤程(fibers
)。虚拟线程是基于协程(coroutine
)的概念,在VM层面上进行管理,而不是依赖于操作系统线程。
为了实现这种轻量级的并发,synchronized
关键字需要绑定到操作系统线程(OS Thread)。每个虚拟线程都会被映射到一个操作系统线程,这样可以避免虚拟线程之间的锁竞争和资源争用。
public static void main(String[] args){
ReentrantLock lock = new ReentrantLock();
Thread.startVirtualThread(() -> {
lock.lock();
});
// 确保锁已经被上面的虚拟线程持有
Thread.sleep(1000);
Thread.startVirtualThread(() -> {
System.out.println("first");
// 会触发Continuation的yield操作
lock.lock();
try {
System.out.println("second");
} finally {
lock.unlock();
}
System.out.println("third");
});
Thread.sleep(Long.MAX_VALUE);
}
虚拟线程中任务执行时候调用Continuation#run()
先执行了部分任务代码,然后尝试获取锁,该操作是阻塞操作会导致 Continuation 的 yield 操作让出控制权
会导致 yield 失败的为上面所述的在
synchronized
块 或synchronized
方法、native
方法 或 外部函数
由此可见,在高并发的情况下,许多虚拟线程都挂载到了平台线程,进入了synchronized
方法,会导致Continuation
的yield
操作失败,则会对作为载体的平台线程进行park
调用,阻塞在载体线程上,此时虚拟线程和平台线程同时会被阻塞
所以在JEPS 444中,官方推荐使用 ReentrantLock 来替换频繁调用的synchronized
块 或 synchronized
方法。对于使用不频繁(如启动时使用)的synchronized
,可以无需替换。
参考资料: