我写了一个线程池和延迟任务工具类,正常使用中,突然一天同事告诉我:你的代码阻塞启动了,快去看看!
线程池是使用的内部框架中提供的,使用者的注入写法也是框架给的,其实本质上是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的,怎么会抛出这个异常呢?
由于我电脑上没问题,而同事电脑上有问题,因此排查时在同事电脑上进行。
在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版本)更新了,然而我没有执行,因此才会出现别人有问题,而我这里正常的现象。
立马抛开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源码的同学应该知道该怎么解决了,为了照顾不熟悉源码的同学,我们继续跟踪。
翻过spring源码的我们知道,ApplicationContextAware的注入时机是:
bean后处理器其中有一个叫 ApplicationContextAwareProcessor,其中的处理方法调用setApplicationContext方法
具体源码调用位置为
知道 setApplicationContext 调用时间点之后,我们就知道为什么会出这个问题了,因为构造方法注入在spring的声明周期中是早于 ApplicationContextAwareProcessor 的,在bean注入阶段,我们是无法通过ApplicationContextAware拿到 spring 容器上下文的,因此才出现这个错误。
既然知道是因为调用点太早,所以解决的时候只需将调用时机推迟即可。
但是!!!
思考过框架设计的同学肯定知道发现这很显然是框架的设计问题,凭什么让我们开发来解决?
没错,这确实是一个框架组的设计缺陷,在设计一个框架时,不应该过分地限制使用者的使用,设计的线程池竟然依赖了spring容器的生命周期,难道我用个线程池还要关心spring容器的启动顺序?在spring容器启动前我们不能调用?显然这不合理,作为一个框架的设计者在设计时就应该依赖分明。
博主校招入职未满3个月,这就是面试时为什么还要问一个校招生,你看过spring源码吗,知道spring运行顺序吗,用过线程池吗,看过线程池源码吗。
如果你看过这些源码,在解决以上问题时,2分钟不到就定位清楚,并把缺陷提给框架组,也知道自己的代码里要如何避开这个坑,而若这些都没看过,在解决这个问题时可能就要花费一段时间了。