前言
最近做项目,实际上是升级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次跳坑的情况。