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次跳坑的情况。

相关推荐
计算机安禾3 小时前
【c++面向对象编程】第44篇:typename与class的区别,依赖类型名与template消除歧义
java·jvm·c++
froginwe113 小时前
Scala 正则表达式
开发语言
时寒的笔记3 小时前
11期_js逆向核心案例解析(sichuan&某理财网)
开发语言·javascript·ecmascript
csbysj20203 小时前
PHP 文件:深入解析与最佳实践
开发语言
JAVA面经实录9173 小时前
Java+SpringAI企业级实战项目完整官方文档(生产终版)
java·开发语言·spring·ai编程
梵得儿SHI3 小时前
Java IO 流进阶:Buffer 与 Channel 核心概念解析及与传统 IO 的本质区别
java·开发语言·高并发·nio·channel·buffer·提升io效率
j_xxx404_3 小时前
Linux线程:从内存分页机制(Page Table/TLB/Page Fault)彻底读懂 Linux 线程本质
linux·运维·服务器·开发语言·c++·人工智能·ai
2301_789015623 小时前
C++_string增删查改模拟实现
java·开发语言·c++
没有逆称3 小时前
Java OOM 问题全解析
java·jvm