likes
comments
collection
share

虚拟线程目前不推荐上生产的个人思考

作者站长头像
站长
· 阅读数 4

2024-3-3 更新: 感谢群友(@大水怪)

虚拟线程目前不推荐上生产的个人思考 感谢 B 站网友 @dreamlike_ocean

虚拟线程目前不推荐上生产的个人思考

1.pin 线程引发的问题比预期严重,需要修改的库繁多

截止目前 Java 21 虚拟线程一些比较严重的 Bug:

  1. Thread.HoldsLock(Object) 这个方法,如果是虚拟线程调用,会在平台线程获取到锁之后,就算切换虚拟线程,也会返回 true:bugs.openjdk.org/browse/JDK-…

2. 默认使用的 ForkJoinPool.common 线程池,如果全部 pin 住,问题很严重。比如synchronized 块外有与块内争用的资源,可能会导致死锁(这个与原来的线程池池化死锁类似)。主要是 monitor enter 的机制与虚拟线程的 Continuation 设计以及 ForkJoinPool 的设计目前不太兼容:bugs.openjdk.org/browse/JDK-…

目前看来,synchronized pin 线程的问题比预期的更严重,很多库,包括 JDK 库本身可能都要兼容:

  1. JDK 库本身:

(1)需要重新看看 AQS 的设计与虚拟线程的兼容性,尤其是队列,防止出现队列调度死锁

(2)需要审查下 ForkJoinPool 的设计,将 ForkJoinPool 作为默认的 Carrier 线程池,是否合适。因为工作窃取导致调度设计困难,并且,ForkJoinPool 天生不支持 Go 那种遇到 Pin 线程新起一个线程的解决方案。我个人觉得,需要那种能手动指定虚拟线程的负载线程池的方案

(3)很多 synchronized 的代码是否要重写,尤其是常用的数据结构以及输出流的地方。

  1. 各种 Java 库的兼容:日常开发离不开 JDBC 库,但是官方的 JDBC 库里面很多 synchronized 以及与虚拟线程的设计不好兼容的没必要的同步队列。

  2. 其实可以考虑 Java 重构 synchronized 不 pin 线程,但是不知道要什么时候了。

2. 非抢占设计与切换消耗不适合 CPU 密集计算型任务:

(1)非抢占式设计:虚拟线程只会在遇到阻塞的时候与底层平台线程分离切换,否则不会切换。比如你有 4 核,同时启动 8 个平台线程的计算任务,每个任务基本上进度是一样的。同时启动 8 个虚拟线程的计算任务,则是先执行 4 个,之后再执行 4 个。

(2)虚拟线程切换的消耗比较大,虽然已经做了很多优化(Continuation 的堆栈增量复制,按需复制,优化虚拟线程 GC 根引用扫描),但是消耗还是很大,下面是一个平台线程执行与虚拟线程执行计算任务的 CPU 采样对比: 虚拟线程目前不推荐上生产的个人思考 虚拟线程目前不推荐上生产的个人思考

3. ThreadLocal很常用很难去掉,ScopedLocal 不能解决所有问题

第一个问题是内存占用太大导致吞吐量下降:ThreadLocal 虽然底层的 Map 是 WeakReference 的,但是设计之初是考虑 Thread 数量有限。在有虚拟线程很大量的时候,这个 Map 是非常消耗内存的。ScopedValue 通过限制作用域,以及值不可变的方式,优化了内存占用的问题。但是,ScopedValue 还处于预览阶段,并且没有解决 ThreadLocal 的所有问题。

没有解决的问题就是第二个问题:之前有很多使用 ThreadLocal 作为资源池的场景(很多库都这么用)。比如说,最早的线程不安全的 SimpleDateFormat(虽然现在已经不怎么用了)。它解决线程不安全的方式,就是或者每次新建一个 SimpleDateFormat,或者使用 ThreadLocal 针对每个线程创建一个独立的。后者肯定消耗比前者小。但是,引入了虚拟线程,就相当于回到了最原来的做法。针对这种资源池的场景(即限制某个线程不安全的资源,每个平台线程创建一个独立使用不并发就行了),其实我们还是想对于平台线程创建。这个对于很多库的影响很多,比如 jackson,jackson 针对这个问题的解决方式是:github.com/FasterXML/j…

其实也不是说用新的 ThreadLocal 去替换,而是换种思路,将对象池化的同时,让程序从池子里获取并在用完的时候放回。相当于明确控制生命周期。但是我们也可以看出,这个对于三方库的改造,也是很大的。