侧边栏壁纸
博主头像
七字节 博主等级

行动起来,活在当下

  • 累计撰写 10 篇文章
  • 累计创建 40 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

ThreadLocal详解

七字节
2025-06-03 / 0 评论 / 0 点赞 / 3 阅读 / 0 字

ThreadLocal详解

ThreadLocal是什么?

ThreadLocal​是Java中实现线程本地变量的一个类,ThreadLocal​填充的变量是当前线程的变量,该变量对其他线程是封闭且隔离的。它为每个线程提供独立的变量副本,确保线程间的数据隔离。

即多个线程访问同一个ThreadLocal对象,但操作的却是自己独有的一份数据。

为何多线程访问同一个ThreadLocal,却可以拿到不同的数据?

每个线程(Thread​类)内部维护一个ThreadLocal.ThreadLocalMap​的实例变量threadLocals​,不同线程访问ThreadLocal​时实际操作的是内部的ThreadLocalMap​,键为ThreadLocal​对象,值为存储的数据。

GC(垃圾回收)之后,ThreadLocalMap的key是否为null?

先说结论,在GC之后,ThreadLocalMap的key也不一定为null。

若要解释,首先需要知道Java的四种引用类型:

  • 强引用:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
  • 软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
  • 弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
  • 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

ThreadLocalMap​会持有一个ThreadLocal​的弱引用,若ThreadLocal​未再被其他引用,则发生GC后ThreadLocal​会被回收,此时Entry​的key变为null,而值被Entry​强引用依然存在,则发生内存泄漏。

ThreadLocal​被强引用,则即使发生GC,ThreadLocal​依然不会被回收,则ThreadLocalMap​的key不为null。例如:

ThreadLocal<Object> threadLocal = new ThreadLocal<>(); // 强引用指向 ThreadLocal 实例
threadLocal.set(s);

在这种情况下,threadLocal​变量持有ThreadLocal​的强引用,ThreadLocal​不会被GC回收。

ThreadLocalMap如何产生索引和解决Hash冲突的?

既然是Map结构,ThreadLocalMap​自然也需要产生索引和解决哈希冲突。

产生索引

int i = key.threadLocalHashCode & (len-1);

关键在于threadLocalHashCode​,每创建一个ThreadLocal​实例,nextHashCode​值便会增长HASH_INCREMENT​,也就是0x61c88647​,这个值是一个斐波那契数,使用斐波那契数的好处是产生的哈希结果非常均匀。

private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

解决哈希冲突

即使使用斐波那契数,依然会有哈希冲突,跟HashMap​使用链表+红黑树解决哈希冲突不同,ThreadLocalMap​没有链表和红黑树,ThreadLocalMap​使用线性探测法来解决哈希冲突。ThreadLocalMap​使用set时大概分为几种情况:

一、

当计算得到的位置Entry​为null​时,直接将Entry​放置在这里,key​为ThreadLocal​,value​为要存的数据。

二、

计算得到的位置Entry​不为null​,且位置上的Entry​的key​,即ThreadLocal​的内存地址与传入的ThreadLocal​内存地址一致,则更新当前位置的数据。

三、

计算得到的位置Entry​不为null​,则往后遍历,直到遇到为null​的Entry​,或者key​相等的Entry​,且中途没有遇到存在Entry​的key​为null​,value​不为null​的情况,则将数据放在此处。

四、

//TODO

ThreadLocal简单例子

在网站项目里,用户的请求可能会跨越多层并且调用多个方法,这多层和多个方法都需要进行用户信息的传递,这时就可以使用ThreadLocal​。

可以在拦截器(Interceptor)处验证用户信息,再将用户信息加入到ThreadLocal​,这样在用户访问的整个流程中都可以通过ThreadLocal​获取用户信息。需要注意在访问结束后,在拦截器处应该删除用户信息,避免内存泄漏

拦截器相关代码:

public class RefreshTokenInterceptor implements HandlerInterceptor {

	//此处省略相关变量

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");
        if (StrUtil.isBlank(token)) {
           return true;
        }
        //2.基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
                .entries(RedisConstants.LOGIN_USER_KEY + token);
        //3.判断用户是否存在
        if(userMap.isEmpty()){
            return true;
        }
        //用户存在
        UserDto userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDto(), false);
        //保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        //......
        return true;
    }

    //......

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //必须要在完成后移除用户,否则可能造成内存泄漏!
        UserHolder.removeUser();
    }
}

UserHolder代码:

public class UserHolder {
    private static final ThreadLocal<UserDto> tl = new ThreadLocal<>();

    public static void saveUser(UserDto user){
        tl.set(user);
    }

    public static UserDto getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

0

评论区