线程池使用InheritableThreadLocal出现数据脏乱分析和解决方案
背景
在测试环境上遇到一个诡异的问题,部分业务逻辑会记录用户ID到数据库,但记录的数据会串,比如当前用户的操作记录会被其他用户覆盖, 而且这个现象是每次重启后一小段时间内就正常
问题
在线程池内部使用了InheritableThreadLocal存放用户登录信息,再获取用户信息后,由于没有及时remove,导致下次请求还是得到旧的用户数据
排查过程
1.通过临时打印日志,确认整个链路中用户ID是否一致
2.确认写日志方法是否有被修改,最后确认是写日志这块开了线程池后导致问题
思考
1.为什么在之前测试过程没有复现
之前依赖的日志记录二方jar包进行过一次升级,原先的版本在记录日志没有开启线程池,后面升级版本后,加入了线程池
2.分析为什么用了InheritableThreadLocal还会出现数据错乱(覆盖)
ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长(在本篇案例中,由于线程池的核心线程没有被回收,一直存在),那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread –> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收;所以出现数据错乱的原因在于核心线程一直没有被回收,然后 InheritableThreadLocal 也未及时remove,导致核心线程一直存放着老
3.本地求证
为了能复现测试环境问题,我写了一个demo在本地去复现这个问题,开启了一个线程器并设置了2个核心线程,调用4次
根据上面的运行日志结果,可以发现第二次的设值并没有真正改变
分析线程池的核心线程是如何不被回收的,源码跟踪如下
1.跟进线程池execute主方法入口
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) //未超过核心线程数,则新增 Worker 对象,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);
}