线程重用问题--ThreadLocal数据错乱

前言

复现Java业务开发常见错误100例–1

项目完整代码:Github地址
《线程重用问题--ThreadLocal数据错乱》

知识点回顾:

ThreadLocal的定义和使用:

ThreadLocal概念以及使用场景

配置文件的读取:

获取配置文件中的key和value;

  1. 创建属性对象
  2. 获取文件流,并进行加载
  3. 遍历文件流获得属性key和value
  4. 属性赋值
Properties p=new Properties();
InputStream stream = clazz.getClassLoader().getResourceAsStream(fileName);
p.load(stream);
p.forEach((k,v)->{
    log.info("{}={}",k,v);
    System.setProperty(k.toString(),v.toString());
});

问题复现

问题描述:代码使用ThreadLocal后,有时获取的用户信息是别人的。
《线程重用问题--ThreadLocal数据错乱》
before:是没有传递值是获取ThreadLocal中的数据;设置用户信息之前先查询一次ThreadLocal中的用户信息
after:是设置ThreadLocal中的值后输出的;设置用户信息之后再查询一次ThreadLocal中的用户信息
由第二个图可以看到before的数据本应该为null,但是现在取的是第一次塞的值1

复现过程

各位可以思考下,接下来进行复现过程:
代码思路比较简单:

  1. 创建SpringBoot项目,实现controller层
  2. 创建ThreadLocal对象
  3. 对ThreadLocal赋值前,获取线程信息和用户值
  4. 对ThreadLocal赋值
  5. 对ThreadLocal赋值后,获取线程信息和用户值
  6. 两者比较即可
  7. 启动前需要读取配置文件(注意点)

代码如下:

/**
 * @author xbhog
 * @describe:
 * @date 2022/8/10
 */

@RestController
@RequestMapping("threadlocal")
public class ThreadLocalDemo {
    private static final ThreadLocal<Integer> CURRENT_USER = new ThreadLocal<Integer>();
    @GetMapping("wrong")
    public Map Wrong(@RequestParam("userId") Integer userId){
        //设置用户信息之前先查询一次ThreadLocal中的用户信息
        String before = Thread.currentThread().getName() + ":" + CURRENT_USER.get();
        //设置ThreadLocal中的用户数据
        CURRENT_USER.set(userId);
        //设置用户信息之后再查询一次ThreadLocal中的用户信息
        String after = Thread.currentThread().getName() + ":" + CURRENT_USER.get();
        //汇总两次的执行结果输出
        Map result = new HashMap();
        result.put("before",before);
        result.put("after",after);
        return result;
    }
}

按理说设置用户信息之前第一次获取的值是null,但是要意识到,程序运行在Tomcat中,执行程序的线程是Tomcat的工作线程,而其工作线程是基于线程池使用的。

由上可知,线程池会使用固定的几个线程,一旦线程重用,那么很有可能会获得前一次或者其他用户请求的遗留值,这时候ThreadLocal中的用户信息就是其他用户的信息。

为了方便演示,在配置文件中设置下tomcat参数,将工作线程池最大线程数设置为1,这样始终是同一个线程在处理请求。

server.tomcat.max-threads=1

配置文件的加载如上,具体代码首行有GitHub地址,欢迎star
通过上述的分析,我们明白了出现的原因,所以只要我们在使用完后,进行删除ThreaLocal中的数据即可。
不光可以防止数据重复,也可以防止内存泄露(虽然出现的概率比较小)。
正确代码如下:

@GetMapping("right")
    public Map Rigth(@RequestParam("userId") Integer userId){
        //设置用户信息之前先查询一次ThreadLocal中的用户信息
        String before = Thread.currentThread().getName() + ":" + CURRENT_USER.get();
        //设置ThreadLocal中的用户数据
        CURRENT_USER.set(userId);
        try{
            //设置用户信息之后再查询一次ThreadLocal中的用户信息
            String after = Thread.currentThread().getName() + ":" + CURRENT_USER.get();
            //汇总两次的执行结果输出
            Map result = new HashMap();
            result.put("before",before);
            result.put("after",after);
            return result;
        }finally {
            //删除ThreadLocal数据,既避免了内存溢出的风险也解决了数据重复的问题
            CURRENT_USER.remove();
        }
    }
    原文作者:xbhog
    原文地址: https://www.cnblogs.com/xbhog/p/16586490.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞