相关文章推荐
憨厚的黑框眼镜  ·  国家知识产权局 奋进新征程 ...·  1 年前    · 
俊逸的企鹅  ·  这届江湖超编了第一二季,这届江湖超编了漫画免 ...·  2 年前    · 
时尚的企鹅  ·  报考指南|杭州师范大学考情分析:导师分析/参 ...·  2 年前    · 
一身肌肉的毛豆  ·  迪士尼 ① 公司深度研究 - 知乎·  2 年前    · 
气宇轩昂的铁链  ·  晚秋黄叶村美诗五首赏析:家在江南黄叶村,相逢 ...·  2 年前    · 
Code  ›  使用CompletableFuture时,那些令人头疼的问题开发者社区
rpc 线程阻塞 线程 completablefutur
https://cloud.tencent.com/developer/article/1765767
含蓄的人字拖
2 年前
作者头像
Java程序猿阿谷
0 篇文章

使用CompletableFuture时,那些令人头疼的问题

前往专栏
腾讯云
开发者社区
文档 意见反馈 控制台
首页
学习
活动
专区
工具
TVP
文章/答案/技术大牛
发布
首页
学习
活动
专区
工具
TVP
返回腾讯云官网
社区首页 > 专栏 > Java快速进阶通道 > 使用CompletableFuture时,那些令人头疼的问题

使用CompletableFuture时,那些令人头疼的问题

作者头像
Java程序猿阿谷
发布 于 2020-12-28 14:38:46
1.7K 1
发布 于 2020-12-28 14:38:46
举报

背景

有一个功能,这个功能里需要调用几个不同的RPC请求,一开始不以为然,没觉得什么,所以所有的RPC请求都是 串行 执行,后来发现部分RPC返回时间比较长导致此功能接口时间耗时较长,于是乎就使用了JDK8新特性CompletableFuture打算将这些不同的RPC请求异步执行,等所有的RPC请求结束后,再返回请求结果。

因为功能比较简单没什么特殊的,所以这里在使用CompletableFuture的时候,并没有自定义线程池,默认那么就是ForkJoinPool。下面看下伪代码:

CompletableFuture task1 = CompletableFuture.runAsync(()->{
             * 这里会调用一个RPC请求,而这个RPC请求处理的过程中会通过SPL机制load指定接口的实现,这个接口所在jar存在于WEB-INFO/lib
            System.out.println("任务1执行");
        CompletableFuture task2 = CompletableFuture.runAsync(()->{
            System.out.println("任务2执行");
        CompletableFuture task3 = CompletableFuture.runAsync(()->{
            System.out.println("任务3执行");
        // 等待所以任务执行完成返回
        CompletableFuture.allOf(task1,task2,task3).join();
        return result;

其实初步上看,这段代码没什么特别的,每个任务都是调用一个RPC请求。初期测试这段代码的时候是通过IDEA启动项目,也就是用的是 SpringBoot 内嵌 Tomcat 启动的,这段代码功能正常。然后呢,代码开始commit,merge。

到了第二天之后,同事测试发现这段代码抛出了异常,而且这个功能是主入口,那么就是说大大的阻塞啊,此时我心里心情是这样的

[图片上传失败...(image-320b40-1608800133019)]

立马上后台看日志,但是却发现这个异常是RPC内部处理时抛出来的,第一反应那就是找上游服务提供方,问他们是不是改接口啦?准备开始甩锅!

image

然后结果就是没有!!! 于是乎我又跑了下项目,测试了一下接口,没问题!确实没问题!卧槽???还有更奇怪的事情,那就是同时装了好几套环境,其他环境是没问题的,此时就没再去关注,后来发现只有在重启了服务器之后,这个问题就会作为必现问题,着实头疼。

问题定位

到这里只能老老实实去debug RPC调用过程的源码了。也就是代码示例中写的,RPC调用过程中,会使用ServiceLoader去找XX接口对应的实现类,而这个配置是在RPC框架的jar包中,这个jar包那自然肯定是在对应微服务的WEB-INFO/lib里了。

这段源码大概长这样吧:

ArrayList list = new ArrayList<String>();
        ServiceLoader<T> serviceLoader = ServiceLoader.load(xxx interface);
        serviceLoader.forEach(xxx->{
            list.add(xxx)
        });

这步执行完后,如果list是空的,那就会抛个异常,这个异常就是前面所说RPC调用过程中的异常了。

到这里,加载不到,那就要怀疑ClassLoader了,先看下ClassLoader加载范围

  • Bootstrap ClassLoader

%JRE_HOME%\lib 下的 rt.jar、resources.jar、charsets.jar 和 class

  • ExtClassLoader

%JRE_HOME%\lib\ext 目录下的jar包和class

  • AppClassLoader

当前应用ClassPath指定的路径中的类

  • ParallelWebappClassLoader

这个就属于Tomcat自定义ClassLoader了,可以加载当前应用下WEB-INFO/lib

再看下ServiceLoader的实现:

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
        return new ServiceLoader<>(service, loader);
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

调用load的时候,先获取当前线程的上下文ClassLoader,然后调用new,进入到ServiceLoader的私有构造方法中,这里重点有一句 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; ,如果传入的classLoader是null(null就代表是BootStrapClassLoader),就使用ClassLoader.getSystemClassLoader(),其实就是AppClassLoader了。

然后就要确定下执行ServiceLoader.load方法时,最终ServiceLoader的loader到底是啥?

  • 1.Debug 通过Sring Boot 内嵌Tomcat启动的应用

在这种情况下ClassLoader是org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader

  • 2.Debug 通过Tomcat启动的应用

在这种情况下ClassLoader是AppClassLoader,通过Thread.currentThread().getContextClassLoader()获取到的是null

真相已经快要接近,为啥同样的代码,Tomcat应用启动的获取到的线程当前上下文类加载器却是BootStrapClassLoader呢?

问题就在于CompletableFuture.runAsync这里,这里并没有显示指定Executor,所以会使用ForkJoinPool线程池,而ForkJoinPool中的线程不会继承父线程的ClassLoader。enmm,很奇妙,为啥不继承,也不知道。。。

问题印证

下面通过例子来证实下,先从基本的看下,这里主要是看子线程会不会继承父线程的上下文ClassLoader,先自定义一个ClassLoader,更加直观:

class MyClassLoader extends ClassLoader{
}

测试一

private static void test1(){
        MyClassLoader myClassLoader = new MyClassLoader();
        Thread.currentThread().setContextClassLoader(myClassLoader);
        // 创建一个新线程
       new Thread(()->{
           System.out.println( Thread.currentThread().getContextClassLoader());
       }).start();
    }

输出

classloader.MyClassLoader@4ff782ab

测试结论: 通过普通new Thread方法创建子线程,会继承父线程的上下文ClassLoader

* 源码分析: 查看new Thread创建线程源码发现有如下代码

if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
            this.contextClassLoader = parent.contextClassLoader;

所以子线程的上下文ClassLoader会继承父线程的上下文ClassLoader

测试二

在 Tomcat容器 环境下执行下述代码

MyClassLoader myClassLoader = new MyClassLoader();
        Thread.currentThread().setContextClassLoader(myClassLoader);
        CompletableFuture<Void> task1 = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getContextClassLoader());
        });

输出

null

但是如果通过main函数执行上述代码,依然是会打印出自定义类加载器

为啥呢?查了一下资料,Tomcat 默认使用SafeForkJoinWorkerThreadFactory作为ForkJoinWorkerThreadFactory,然后看下SafeForkJoinWorkerThreadFactory源码

private static class SafeForkJoinWorkerThread extends ForkJoinWorkerThread {
        protected SafeForkJoinWorkerThread(ForkJoinPool pool) {
            super(pool);
            this.setContextClassLoader(ForkJoinPool.class.getClassLoader());
    }

这里发现,ForkJoinPool线程设置的ClassLoader是java.util.concurrent.ForkJoinPool的类加载器,而此类位于rt.jar包下,那它的类加载器自然就是BootStrapClassLoader了

问题解决

解决方式一:

ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
 
推荐文章
憨厚的黑框眼镜  ·  国家知识产权局 奋进新征程 建功新时代•非凡十年 新时代新华章
1 年前
俊逸的企鹅  ·  这届江湖超编了第一二季,这届江湖超编了漫画免费在线观看全集 - 快看漫画
2 年前
时尚的企鹅  ·  报考指南|杭州师范大学考情分析:导师分析/参考书单/历年真题/招考分析【附专项班计划】 - 知乎
2 年前
一身肌肉的毛豆  ·  迪士尼 ① 公司深度研究 - 知乎
2 年前
气宇轩昂的铁链  ·  晚秋黄叶村美诗五首赏析:家在江南黄叶村,相逢俱是画中人|曹雪芹|苏轼|陆游|柳宗元_网易订阅
2 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号