线程池使用InheritableThreadLocal踩坑总结

发布时间 2023-08-11 14:10:18作者: 甜菜波波

一、缘起

某天测试环境更新后,有小伙伴反应页面会随机性的发生请求参数为空的情况(request.getParamter为空),但是前端的参数是传了的,而且不能稳定重现,需要在页面上经过一番操作之后才会发生,而当问题重现之后,之前那些可用的页面就变得不可用了,然后就会在可用和不可用之间交替......

我接到问题的第一反应是

线程池使用InheritableThreadLocal踩坑总结

二、踩坑

2.1 寻找罪魁祸首

代码中request为空,但是前端有传递,第一时间想到的就是线程切换导致ThreadLocal传递出现问题。

然而这个坑我们之前是踩过的,并且已经在切面中手动改成了可继承的线程变量

HttpServletRequest servletRequest = WebUtil.getRequest();
HttpServletResponse servletResponse = WebUtil.getResponse();
//声明子线程的时候,这些属性不会继承,手动赋值成可继承的属性
ServletRequestAttributes attributes = new ServletRequestAttributes(servletRequest, servletResponse);
RequestContextHolder.setRequestAttributes(attributes, true);
LocaleContextHolder.setLocaleContext(LocaleContextHolder.getLocaleContext(), true);

难道切面没生效?

可是经过调试发现,这段代码是进入并执行了的。

通过查看提交记录发现,切面中有人加了这么一段代码(没错就是我)

ExecutorService TIMEOUT_EXECUTOR_POOL = new ThreadPoolExecutor(
    Runtime.getRuntime().availableProcessors() + 1, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
    new SynchronousQueue<>(), ThreadUtil.newNamedThreadFactory("TIMEOUT_EXECUTOR_POOL", false)
);
FutureTask<Object> futureTask = new FutureTask<>(() -> {
    try {
        return joinPoint.proceed();
    } catch (Exception ex) {
        throw ex;
    } catch (Throwable throwable) {
        throw new Exception(throwable);
    }
});
TIMEOUT_EXECUTOR_POOL.submit(futureTask);

为了增加超时时间的控制,我用FutureTask把执行的代码包装了一层

在这里打断点调试,发现在报错的时候,futureTask外部request参数有值,进入后参数为空。

但是,偶尔也是会有值的!有值的时候就是页面正常的时候。

线程池使用InheritableThreadLocal踩坑总结

2.2 找出作案动机(原因)

我们先看下InheritableThreadLocal是怎么实现线程变量可继承的

在Thread的init()方法中有一段代码

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    //省略部分代码
   
    //如果父线程inheritableThreadLocals不为空,则保存下来
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    
    //省略部分代码
}

可以看到InheritableThreadLocal是在Thread创建的时候继承的。

而我们知道线程池的作用就是“缓存”线程来避免线程频繁的创建和销毁,所以如果在线程池中使用InheritableThreadLocal,只有第一个创建线程时的请求是可以用的,后续请求的InheritableThreadLocal都跟第一个请求一样,不会再改变。

至此,问题原因找到了,因为我创建线程池的时候初始化了CPU核数+1个线程,所以开始一些请求是正常的,后续当这些线程都使用了之后,就会因为InheritableThreadLocal不同导致错误。而且我们自己测试的时候是在几个按钮中重复点击,如果线程的第一个请求是/user/query,当你再次发起这个请求的时候如果刚好分配的是这个线程,页面就是正常的,于是就出现页面时好时坏的情况.

三、填坑

OK,出现问题的地方找到了,下面来解决

1、直接注释掉这段超时控制的代码

这个实在是太粗暴了,只适合紧急情况下使用,作为一个有追求的程序猿,我是不可能这么做的

2、不用线程池,直接new Thread

既然是线程池复用导致的问题,不用线程池就可以解决

3、使用阿里的TransmittableThreadLocal

https://github.com/alibaba/transmittable-thread-local

阿里巴巴开源了一个类似于InheritableThreadLocal的库,就是用来在线程池中使用,有兴趣的可以瞅一眼

转自:http://www.voycn.com/article/xianchengchishiyonginheritablethreadlocalcaikengzongjie