ThreadLocal变量Springboot下踩坑记录

在SpringBoot Web项目中使用ThreadLocal变量记录用户信息,踩坑记录。

一、简介ThreadLocal变量

简单描述ThreadLocal变量:ThreadLocal变量是属于线程独有的遍历,不同线程之间相互隔离,每个线程只可访问属于自己线程的ThreaLocal变量。

简单使用:

public static ThreadLocal<String> userName = new ThreadLocal<>();
//赋值
userName.set("张三");
//取值
String name = userName.get();
//移除
userName.remove();

核心源码:

    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //可以简单理解为线程的一个Map属性
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //map中put值,key 为ThreadLocal对象
            map.set(this, value);
        else
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取线程对应ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //值null
        return setInitialValue();
    }

     public void remove() {
        //根据线程获取对应的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
            //从ThreadLocalMap中移除对应entry
             m.remove(this);
     }

简单概述下核心点:

1、每个线程都有一个ThreadLocalMap对象(不深入可以就当成一个普通map)

2、ThreadLocalMap中存储ThreadLocal相关的遍历和值,存储方式ThreadLocal为key,值为value的键值对

3、ThreadLocalMap变量的生命周期与线程生命周期相关

二、SpringBoot下踩坑分析

项目简介:项目为使用springboot的web项目,使用内置tomcat。用户信息存储在redis中使用token为key,同时会使用ThreadLocal变量保存获取到的用户信息。获取用户信息时先从ThreadLocal获取,获取不到再从redis获取,然后写入ThreadLocal。

出现问题:多用户并发访问时,出现权限混乱,B用户获取到A用户的用户信息

问题原因:从ThreaLocal变量中拿到的用户信息出现错误,根本原因与线程池有关,Springboot中内嵌Tomcat处理请求使用的线程池,即从线程池中获取空闲线程使用,使用结束的线程并不会销毁(默认初始化工作线程10),因为在使用ThreadLocal时没有执行remove方法,所以线程的ThreadLocalMap对象初始化后就始终存在,后续再从线程池中拿到之前使用过的线程后,其中值依然存在,从而导致错误。

处理方法:线程结束后对ThreadLocal变量执行remove方法,springboot web项目可以使用拦截器处理。

如下是对出错部分的模拟测试:

public class TestThreadLocal {
    public static ThreadLocal<String> userName = new ThreadLocal<>();
    public static AtomicInteger count = new AtomicInteger();
    @GetMapping("test")
    public String getThreadInfo(){
        int num = count.incrementAndGet();
        String currentThreadName = Thread.currentThread().getName() +"==========="+ num;
        String localThreadName = this.getLocalThreadInfo(currentThreadName);
        return String.format("当前线程:%s, ThreadLocal:%s",currentThreadName, localThreadName);
    }
    private String getLocalThreadInfo(String name){
        String threadName = userName.get();
        if(StringUtils.hasText(threadName)){
            log.info("当前线程:{},ThreadLocal:{}",name,threadName);
        }else{
            threadName = name;
            log.info("当前线程:{},时间:{}", threadName, System.currentTimeMillis());
            userName.set(threadName);
        }
        return threadName;
    }
}
//输出
当前线程:http-nio-8080-exec-1===========1,时间:1688627451142
当前线程:http-nio-8080-exec-2===========2,时间:1688627451603
当前线程:http-nio-8080-exec-3===========3,时间:1688627452039
当前线程:http-nio-8080-exec-4===========4,时间:1688627452335
当前线程:http-nio-8080-exec-5===========5,时间:1688627452648
当前线程:http-nio-8080-exec-6===========6,时间:1688627452931
当前线程:http-nio-8080-exec-7===========7,时间:1688627453249
当前线程:http-nio-8080-exec-8===========8,时间:1688627453649
当前线程:http-nio-8080-exec-9===========9,时间:1688627454380
当前线程:http-nio-8080-exec-10===========10,时间:1688627455357
当前线程:http-nio-8080-exec-1===========11,ThreadLocal:http-nio-8080-exec-1===========1
当前线程:http-nio-8080-exec-2===========12,ThreadLocal:http-nio-8080-exec-2===========2

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值