ThreadLocalMap原理和使用

业务场景

ThreadLocal 用来创建线程的局部变量,每一个线程都拥有该变量的独立副本,互相不会干扰。

可以存储上下文的基本信息,比如 userId。

实际的应用场景:

比如用户评论了某个文章,肯定要记录用户的 ID,但是用户请求过来正常是不携带 ID,在拦截器中解析 token,ID 存储到 ThreadLocal 中去,在插入的时候就可以拿到 ID,这样就减少了参数的传递。

使用方式:

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();
    }
}

在登录拦截器中 获取到请求头中的 token. 将用户信息保存到 ThreadLocal 中去。

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 1.获取请求头中的token
    String token = request.getHeader("authorization");
    if (StrUtil.isBlank(token)) {
        return true;
    }
    // 2.基于TOKEN获取redis中的用户
    String key  = LOGIN_USER_KEY + token;
    Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
    // 3.判断用户是否存在
    if (userMap.isEmpty()) {
        return true;
    }
    // 5.将查询到的hash数据转为UserDTO
    UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
    // 6.存在,保存用户信息到 ThreadLocal
    UserHolder.saveUser(userDTO);
    // 7.刷新token有效期
    stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
    // 8.放行
    return true;
}

程序运行完成后需要移除 Threadlcoal 的数据,避免内存泄漏。可以在拦截中移除。

  @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }

底层的结构

ThreadLocal 里面有一个结构 ThreadLocalMap 用来存储数据。

每一个 Thread 对应一个 ThreadLocalMap。

map 里面是一个 Entry 数组,index 是根据 TheadLocal 计算出来的数组下标,value 就是用户存储的值进去。

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0

createMap 方法

get 方法

内存泄露

内存泄露的概念:

内存泄露(Memory Leak)指的是在程序运行过程中,动态分配的内存空间没有被及时释放,导致这些内存资源一直被占用,最终可能导致系统内存耗尽。简单来说,就是程序中不再使用的内存没有被回收,但程序依然持有该内存的引用,造成内存资源的浪费。

ThreadLocal 在使用中可能会出现内存泄露的情况,

正常 Thread 线程执行任务结束后,Thread 线程和 ThreadLocalMap 之间的引用关系就不存在了,GC 会 自动回收掉。

但是我们在实际开发中经常会用线程池来复用线程,这两个之间的引用就会一直存在,线程结束之后并没有销毁,而是直接复用了。

因为 Entry 是弱引用,继承了 WeakReference。

Key 是可以被自动回收的,但是 Value 还在被引用,Value 不能回自动回收,这种情况下就会又内存泄露的风险。

面试题

threadlocal 的使用场景

threadlocal 可以用来保存用户的上下文信息 比如 UserId,每一个线程都拥有该变量的独立副本,互相之间不会干扰。

Threadlocal 的原理:每一个线程内存都有一个 threadlocalmap,底层是一个 entry 数组,key 是 threadlocal 对象,值是一个 object 类型。

threadlocal 会产生的问题

产生的问题:内存泄露,正常情况来说请求结束之后线程就会销毁了,thread 和 threadlocalmap 之间就没有引用关系了。但是在实际中会使用了线程池,复用线程,减少资源的开销。这个引用关系就一直存在,而键值对 entry 是弱引用,value 是强引用。ThreadLocalMap.Entrykey(即 ThreadLocal 实例)是一个 弱引用。这意味着,当 ThreadLocal 实例没有强引用时,会被垃圾回收 ,但是 value 不会被回收一直会在内存中,这样会导致内存溢出的情况。

可以在拦截器中手动清理 threadlocal 里面的 value。

子进程能访问父进程的 threadlocal 值吗?

操作系统为每一个进程都分配了独立的内存空间,一个进程中内容不能背其他进程访问。

ttl 实现子进程访问父进程的资源

https://www.cnblogs.com/intotw/p/14740215.html

#Java面试##我的实习日记##threadlocal#
牛牛的面试专栏 文章被收录于专栏

牛牛的面试专栏,希望自己在25年可以拿到一份大厂的SP Offer 你的点赞和收藏都是我持续更新的动力

全部评论

相关推荐

08-22 23:47
已编辑
小米_软件开发工程师
嗨,牛客的朋友们好。我是某二本院校25届的学生,和很多人一样,刚上大学时迷茫又焦虑。学校不够亮眼、资源有限,大厂仿佛离我们非常遥远。但也就是这样的起点,逼着我早早想清楚:除了拼命,没有第二条路。我从大二下学期开始觉醒,定下目标:冲Java后端。那时候啥也不会,连SpringBoot怎么配都不懂。于是开始疯狂刷算法(LeetCode+牛客题库刷穿3遍)、啃八股(Java并发、JVM、MySQL、Redis翻来覆去背+理解)、做项目(仿电商、秒杀)。**第一段实习:小厂起步,但无比感恩**2023年暑假,我只拿到一家不到50人小公司的后端实习,写CRUD、调试老旧系统。虽然技术栈旧,但让我真正理解了工程是怎么回事,也认识了两位愿意带我往前走的师兄。**第二段实习:蓄力冲进大厂**2024年春季,我用小厂经历+不断迭代的面试经验,终于搏到一个中厂实习机会(这里就不点名了)。那段时间几乎是白天干活、晚上继续刷题+补充分布式/微服务知识,经常熬到凌晨。但也正因为这段经历,让我初步拥有了“高并发”、“系统设计”的实战意识。**2025年秋招:逆袭的开始**8月开始投递,9月面试密集到来。小米面试官问得很深,从JUC到Spring源码、从MySQL调优到分布式事务,几乎把我“扒了一层皮”。但幸好,两年来的积累没有白费——那些反复默写过的八股、那些调试过的线上问题、那些刷了又刷的算法题,全部成了我逆天改命的底气。12月,我收到了小米Java后端的Offer。我知道很多二本、双非的同学都在焦虑,甚至自我怀疑。但我想用我的经历告诉你:**学校只是起点,绝不是终点。**你刷的每一道算法、看的每一篇八股、写的每一行代码,都在默默为你铺路。如果你也在冲Java后端,也在等一个机会——**小米2026届校招内推码:BA6T8F1**欢迎投递,期待和你成为同事。(PS:投了之后可以私信我,帮你查进度+分享面经细节。我们都不是天生强者,但我们可以选择绝不低头。)
点赞 评论 收藏
分享
优财云链Java实习8.22面试1.谈谈你对面向对象的看法2.线程池的核心参数3.核心线程数和最大线程数的区别4.什么样的情况需要设置索引5.你了解的一些索引优化的方式6.网络的分层结构7.tcp在那一层,讲讲tcp的三次握手8.socket套接字了解吗9.设计模式了解过吗10.动态代理了解吗11.讲一下反射12.spring中哪里用到了反射13.那你提到了面向切面编程,讲讲aop的原理,反射在这里怎么用的14.那你刚说到了spring控制反转,那么是对什么进行了控制反转15.bean的生命周期了解吗16.说一下springboot中常用注解17.springboot是开发手脚架,那你觉得为什么他能简化开发搭建一个项目的过程18.自己搭过springboot项目吗19.springcloud里的Feign用过吗(天呐我竟然不知道这个怎么读,没听出来是这个远程调用,答了没用过)20.maven的依赖冲突怎么解决21.说一下你的项目吧22.在什么样的场景下你会选择做一个异步的接口(还需了解一下,调用第三方服务异步,如果调用出错怎么将信息返回上游)23.现有一个下游系统,它处理并发是有限制的,但上游系统请求无限制,那么中间层如何设置保证下游系统不崩掉(用线程池去做)24.问个人情况,有无主动学习能力。25.学习过程中遇到问题怎么处理的26.学习新技术的过程中,更在乎功能还是原理27.比如说?28.线程池具体实现,线程池参数中的队列有几种29.评价一下自己吧30.将大局逆转吧!换我提问,结束了。
查看29道真题和解析
点赞 评论 收藏
分享
评论
点赞
7
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务