首页 > ThreadLocalMap原理和使用
头像
幻想offer的牛牛
发布于 03-07 08:59 江苏
+ 关注

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

全部评论

(1) 回帖
加载中...
话题 回帖

近期热帖

热门推荐