JDK8升级JDK17使用CompletableFuture在线程中classloader的变化

前言

最近做项目,实际上是升级JDK,从JDK8升级为17,本身JDK的兼容性没什么问题,一般而言高版本的JDK完全兼容低版本,这个在class文件这个里面体现了,java新特性编译class文件表现为在高版本的JDK只会增加相应的定义,一般不会废弃,这个特性为向下兼容。但是在CompletableFuture线程的classloader确有区别,这次跳坑了。简单为JDK9开始的模块化设计和CompletableFuture的线程初始化会对系统的cpu资源的判断。

准备示例

JDK8+springboot 2.7

JDK17+springboot 2.7

java 复制代码
@Component
public class Demo {

    @PostConstruct
    public void init(){
        System.out.println("main thread: " + Thread.currentThread().getContextClassLoader().getClass().getCanonicalName());
        CompletableFuture.runAsync(() -> {
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            System.out.println("classloader: " + classLoader.getClass().getCanonicalName());
        });
    }
}

其实springboot jar启动也有classloader,这也会造成一定影响。下面分别使用idea模式和jar模式使用,至于cpu资源的影响,使用虚拟机模拟2C。

JDK8

使用idea直接main方法执行:

可以看到classloader为appclassloader,如果打成jar呢,试一试

可以看到classloader变了,使用了spring jar打包封装的classloader,但是都是一样的。

JDK17

使用idea,classloader没区别

使用jar部署试试

CompletableFuture线程的classloader是appclassloader,这里就不一样了。

CPU资源与classloader的区别

笔者是mac电脑m4芯片,10核CPU,不能模拟出问题的情况,其实当使用企业级开发平台时,测试和生产资源是不一样的,比如测试2C,生产4C,问题又出现了。笔者使用虚拟机模拟,当然这个就跟JDK版本无关了,笔者这里使用JDK17。

分配2 processor cores

执行jar,果然classloader一样。

原因分析

cpu资源分析

这个需要分析源码,以JDK8为例

java 复制代码
    /**
     * Returns a new CompletableFuture that is asynchronously completed
     * by a task running in the {@link ForkJoinPool#commonPool()} after
     * it runs the given action.
     *
     * @param runnable the action to run before completing the
     * returned CompletableFuture
     * @return the new CompletableFuture
     */
    public static CompletableFuture<Void> runAsync(Runnable runnable) {
        return asyncRunStage(asyncPool, runnable);
    }

这里的关键在于asyncPool的初始化,在没有人为设置的情况下,默认设置是什么

其实是否使用forkjoin是根据并行化决定的,并行化>1则使用forkjoin,否则使用自定义线程

果然简单,😅这解释了为什么2C的情况下classloader一样的情况,其实就是使用了自定义的线程,不是forkjoin,看看forkjoin并行度的判断为什么与2C有关系。

静态成员变量

复制代码
static final ForkJoinPool common;

那么就是static代码块初始化

定义了forkjoin的初始化和并行度变量

复制代码
static final int SMASK        = 0xffff;  

初始化的并行变量为-1,然后在判断<0,和对并行变量赋值核心数-1

这里就是把并行度设置了:

默认什么都不设置的情况为核心数-1,

2C刚好就是1

1C就是0,然后赋值1,结果也是1

max_cap值比较大,基本上不可能超过

这就解释了,为什么2C和4C出现巨大的差异,所以测试环境的资源不能省啊,😅。

forkjoin在模块化时的设计

先分析jdk8和jdk17的区别,实际上从jdk9开始就模块化设计了,模块化设计的结果就是

forkjoin在JDK9开始,是模块化加载的,这导致forkjoin的线程的classloader为appclassloader,而在JDK8的时候为主线程的classloader加载,当然这个问题仅在jdk8升级时出现,一旦升级好,就不存在这个问题了,为了这个问题的解决,一般建议对forkjoin设置单独的线程池,规避问题。

总结

其实这个问题非常简单,一般而言也不会出现问题,但是java虚拟机的classloader加载机制注定在特殊情况会出现加载不到的情况,毕竟双亲委派机制的特性。如果刚好遇到jdk升级或者资源的问题,那么这个问题会变得极其复杂,到底是什么问题,其实如果刚好jdk8升级,加上测试生产资源的区别,那么这个问题可能会出现2次跳坑的情况。

相关推荐
SL_staff14 分钟前
物模型三要素误用避坑指南:属性、事件、服务的正确建模逻辑
java
一条泥憨鱼19 分钟前
【Redis | 黑马点评】短信登录功能 源码深度分析
java·redis·后端·session·黑马点评
威哥爱编程23 分钟前
【AI全栈12-12】Spring Boot 3.x → 4.x 迁移实操:Jakarta 11、Jackson 3 与 AI 2.0
java·spring boot·后端
微笑星河q27 分钟前
C++类和对象(下)初始化列表、类型转换、static、友元、内部类、匿名对象
开发语言·c++
威哥爱编程28 分钟前
【AI全栈12-11】Spring Boot 把 AI 接口真正上线扛量:限流 / 降级 / 可观测
java·spring boot·后端
小蒜学长29 分钟前
基于SpringBoot+uni-app的闪洁家政系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·uni-app·家政服务
凤年徐30 分钟前
【Python】Python 语法速通指南
开发语言·python
quantdash_cc34 分钟前
量化数据清洗应该放在数据层还是策略层?从系统架构看清职责边界
开发语言·python·数据分析·系统架构·量化交易·股票数据·quantdash
敲代码的彭于晏44 分钟前
2026 K8s 零基础入门:手把手教你一步步搭建 Kubernetes 集群
java·运维·全栈
|_⊙1 小时前
C++ 类和对象零散知识点
开发语言·c++