线程池使用InheritableThreadLocal出现数据脏乱分析和解决方案

线程池使用InheritableThreadLocal出现数据脏乱分析和解决方案

背景

  在测试环境上遇到一个诡异的问题,部分业务逻辑会记录用户ID到数据库,但记录的数据会串,比如当前用户的操作记录会被其他用户覆盖, 而且这个现象是每次重启后一小段时间内就正常

问题

  在线程池内部使用了InheritableThreadLocal存放用户登录信息,再获取用户信息后,由于没有及时remove,导致下次请求还是得到旧的用户数据

 

排查过程

  1.通过临时打印日志,确认整个链路中用户ID是否一致

  2.确认写日志方法是否有被修改,最后确认是写日志这块开了线程池后导致问题

 

思考

1.为什么在之前测试过程没有复现

  之前依赖的日志记录二方jar包进行过一次升级,原先的版本在记录日志没有开启线程池,后面升级版本后,加入了线程池

 

2.分析为什么用了InheritableThreadLocal还会出现数据错乱(覆盖)

   ThreadLocalThreadLocalMap 中是以一个弱引用身份被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);
    }
hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » 线程池使用InheritableThreadLocal出现数据脏乱分析和解决方案