为什么面试要求看过源码——案例:由于设计不当导致线程池execute方法抛异常

问题描述

我写了一个线程池和延迟任务工具类,正常使用中,突然一天同事告诉我:你的代码阻塞启动了,快去看看!

问题背景

线程池是使用的内部框架中提供的,使用者的注入写法也是框架给的,其实本质上是JDK的 ThreadPoolExcute,我能决定的几乎只是对线程的命名。

之前一直是可以运行的,发生问题的某一天我的电脑上可以正常启动,而其他同事的电脑上则无法启动web容器(IllegalStateException)。

我写的延迟任务是依赖线程池的,有一个搬运工线程,负责从delayQueue中拿任务放入线程池中。

搬运工线程

@Override
    public void run() {
        while (true) {
            try {
                // 从延时队列中获取任务
                DelayTask<?> delay = delayQueue.take();
                Runnable task = delay.getTask();
                if (task != null) {
                    threadPool.execute(task);
                }
            } catch (Exception e) {
	               ...
            }
        }
    }

这个线程是在一个bean的构造方法中执行的:在启动时把这么一个runnable放入了线程池

	@Autowired
    public SdmcDelayTaskPorter(XxxxThreadPool threadPool) {
        this.threadPool = threadPool;
        this.threadPool.execute(this);// 这里报错!!!
    }

代码中注释位置报错
IllegalStateException(“can not find application context.”);
线程池虽然是框架组提供的,但是excute方法是JDK的,怎么会抛出这个异常呢?

问题定位

由于我电脑上没问题,而同事电脑上有问题,因此排查时在同事电脑上进行。

1. JDK源码部分

在idea里进入excute具体调用的方法,确实是jdk的代码,这点我们不用怀疑,继续跟踪就是了。
jdk ThreadPoolExcute#execute方法所有代码如下:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))//这里!!!!!!!!!!!!!!!
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

代码怎么读呢?一眼望去,都是jdk写的,大概率不会是jdk问题,但这个方法里调用了其他方法,不保证其他方法里没有给我们留扩展,看方法名称发现addWorker(command, true)这个地方最可疑(已标注)
进入addWorker方法后,代码很长,看名字也没调用什么方法,唯一值得关注的是new Worker(firstTask);
进入这个方法看,发现果然有更多的方法调用

	Worker(Runnable firstTask) {
            setState(-1);
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);// 这里!!!
        }

其中 getThreadFactory() 只是反回了线程池的一个内部变量线程工厂,是由初始化的时候决定的,newThread方法是线程工厂的,看到这里,我们就知道该关注什么了!

暂时猜测:框架代码出问题了,因为同事执行了maven reimport,导致依赖框架代码(snapshot版本)更新了,然而我没有执行,因此才会出现别人有问题,而我这里正常的现象。
2. 框架源码部分

立马抛开jdk源码,去找创建线程池的代码,看线程工厂是什么,在创建新线程时候做了什么事情。找到具体的线程工厂(框架代码)后,发现以下:

public Thread newThread(Runnable r) {
        TraceRunnable traceRunnable = new TraceRunnable((Tracing)ApplicationContextExt.getBean(Tracing.class), r.toString(), r);
        ...
    }
发现框架想帮我们做到追踪线程的运行状态,和监控线程的运行信息,于是第一行做了一个封装。

果然,第一行代码里居然有个从容器里getBean的操作
这个类实现了 ApplicationContextAware 接口,同时实现也很简单正常的保存了一下
这里放出ApplicationContextExt的 getBean代码:

getBean代码

	public static <T> T getBean(Class<T> cls) {
        if (context == null) {
            throw new IllegalStateException("can not find application context.");// 这里抛出了异常!!!
        } else {
            try {
                return context.getBean(cls);
            } catch (BeansException var2) {
                return null;
            }
        }
    }

终于来到出错的地点!发现因为context为null,而context唯一设置值的时刻为接口方法 setApplicationContext,实现如下

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

经过以上分析,看过spring源码的应该知道,setApplicationContext 是由spring调用的,调用点是在bean初始化完毕后执行的。
其实看到这里熟悉spring源码的同学应该知道该怎么解决了,为了照顾不熟悉源码的同学,我们继续跟踪。


3. spring源码部分

翻过spring源码的我们知道,ApplicationContextAware的注入时机是:
bean后处理器其中有一个叫 ApplicationContextAwareProcessor,其中的处理方法调用setApplicationContext方法
具体源码调用位置为

  • 入口:AbstractApplicationContext#refresh,调用了 prepareBeanFactory
  • 进入 prepareBeanFactory方法,有一行代码如下
    • beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
    • 该方法即为调用

知道 setApplicationContext 调用时间点之后,我们就知道为什么会出这个问题了,因为构造方法注入在spring的声明周期中是早于 ApplicationContextAwareProcessor 的,在bean注入阶段,我们是无法通过ApplicationContextAware拿到 spring 容器上下文的,因此才出现这个错误。

问题解决

既然知道是因为调用点太早,所以解决的时候只需将调用时机推迟即可。

但是!!!
思考过框架设计的同学肯定知道发现这很显然是框架的设计问题,凭什么让我们开发来解决?

没错,这确实是一个框架组的设计缺陷,在设计一个框架时,不应该过分地限制使用者的使用,设计的线程池竟然依赖了spring容器的生命周期,难道我用个线程池还要关心spring容器的启动顺序?在spring容器启动前我们不能调用?显然这不合理,作为一个框架的设计者在设计时就应该依赖分明。


回顾

博主校招入职未满3个月,这就是面试时为什么还要问一个校招生,你看过spring源码吗,知道spring运行顺序吗,用过线程池吗,看过线程池源码吗。

如果你看过这些源码,在解决以上问题时,2分钟不到就定位清楚,并把缺陷提给框架组,也知道自己的代码里要如何避开这个坑,而若这些都没看过,在解决这个问题时可能就要花费一段时间了。

你可能感兴趣的:(java,生活记录,spring)