前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >实战: 多线程下使用ThreadLocal 实现上下文存储用户信息

实战: 多线程下使用ThreadLocal 实现上下文存储用户信息

原创
作者头像
一点点
修改2025-02-01 15:44:47
修改2025-02-01 15:44:47
920
举报

ThreadLocal是什么?

ThreadLocal是java语言中jdk的一个类包,它的主要作用就是为线程提供本地变量,存储于线程内部的一些变量。因此ThreadLocal就不存在多线程共享变量产生的安全问题,也就是说它是线程安全的。所以我们基于此进行线程上下文的存储就是天然的优势了。

来一个简单的demo

我们做一个简单的demo测试。声明一个静态的threadLocal。

在main方法中,启动两个线程,线程1先set值为100,然后sleep 2秒。线程2先sleep 1秒然后set值为200.

这样的目的是确保线程1先赋值后,线程2再赋值,此时打印线程2值为200,线程1值为100 。

这也就说明了,虽然线程2后赋值,但没有覆盖线程1的值,也就是说明2个线程的值互相不干扰。

代码语言:txt
复制
public class ThreadLocalDemo {
    // 定义一个ThreadLocal变量
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 创建并启动两个线程
        Thread thread1 = new Thread(() -> {
            // 线程1设置ThreadLocal变量的值
            threadLocal.set(100);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 线程1获取并打印ThreadLocal变量的值
            System.out.println("Thread1: " + threadLocal.get());
        });

        Thread thread2 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 线程2设置ThreadLocal变量的值
            threadLocal.set(200);
            // 线程2获取并打印ThreadLocal变量的值
            System.out.println("Thread2: " + threadLocal.get());
        });

        thread1.start();
        thread2.start();
    }
}

打印结果

通过get看看内部结构

get方法

我们通过get来看一下ThreadLocal内部到底如何实现的:

  • 1、获取当前线程
  • 2、通过getMap(t)获取ThreadLocalMap,这里我们看到是通过线程来获取的的map。这里肯定是通过线程信息做为key,具体数据做为值来存储的。而且每一个线程都有一个ThreadLocalMap。这个map应该就是存具体数据的。
  • 如果map存在,则通过getEntry(this)来获取Entry ,通过这个我们就知道了,具体的数据是存在Entry中的。
  • 我们大概可以梳理一下了:每一个线程都有一个ThreadLocalMap ,而ThreadLocalMap中具体的数据是一个Entry[]数组存储的,通过当前线程实例的hashCode信息可以计算出具体的数组的索引值。
  • 接下来,如果Entry不为空,则直接返回数据,如果为空的话,则会执行后续的初始化值。
代码语言:txt
复制
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 如果ThreadLocalMap不为空
    if (map != null) {
        // 在ThreadLocalMap中查找当前ThreadLocal对象对应的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果Entry不为空
        if (e != null) {
            // 将Entry的value转换为泛型类型T,并返回
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果ThreadLocalMap为空或者Entry为空,则设置初始值并返回
    return setInitialValue();
}
代码语言:txt
复制
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

基于上面的逻辑,我们可以看一下关系图

我们继续看一下初始化值:

初始化值

  • 调用initialValue初始化值,这个是一个子类去实现的方法,默认为null,我们可以忽略。
  • 初始化值的第二步我们可以看到还是通过线程获取map数据。
  • 当map不为null时,直接赋初始化值。
  • 当map为空,则创建一个map
  • 接着返回初始化值。

到这里,get方法就结束了。

代码语言:txt
复制
private T setInitialValue() {
    // 调用initialValue方法获取初始值
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 如果ThreadLocalMap不为空
    if (map != null) {
        // 在ThreadLocalMap中设置当前ThreadLocal对象对应的值
        map.set(this, value);
    } else {
        // 如果ThreadLocalMap为空,则创建一个新的ThreadLocalMap,并设置值
        createMap(t, value);
    }
    // 如果当前ThreadLocal对象是TerminatingThreadLocal的实例
    if (this instanceof TerminatingThreadLocal) {
        // 将其注册到TerminatingThreadLocal的管理器中
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    // 返回初始值
    return value;
}

set方法

set我们就简单看下源码就明白了,其实上面的初始化方法也就是set方法的执行流程,只是数据是通过参数传递进来的。

代码语言:txt
复制
 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

实战

虽然ThreadLocal很有用,但是实际使用起来并不复杂。

创建用户授权上下文工具

我们首先创建一个ThreadLocal存储当前线程的上下文信息,然后提供一个set方法和一个get方法,方便存储和获取。

注意,这个clear方法是必须要的:再线程池的情况下需要手动清除,不然线程实例重复获取数据会重复。

代码语言:txt
复制
public class UserAuthContext {
    /**
     * 用户的登录相关的ThreadLocal
     */
    private static ThreadLocal<UserDO> userLoginInfo = new ThreadLocal<>();

    public static void setUserLoginInfo(UserDO userInfo) {
        userLoginInfo.set(userInfo);
    }

    /**
     * 获取用户登陆相关信息
     */
    public static UserDO getUserInfo() {
        return userLoginInfo.get();
    }

    /**
     * 清空用户登陆相关信息
     */
    public static void clear(){
        userLoginInfo.remove();
    }
}

拦截器验证授权并在上下文赋值用户信息

代码语言:txt
复制
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        //验证、鉴权

        //获取到用户信息, 存储用户信息到上下文
        UserAuthContext.setUserLoginInfo(userInfo);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }
}

使用用户信息

在实际需要获取用户信息的地方,使用这个方法直接获取线程中存放的用户信息

代码语言:txt
复制
//获取登录用户信息
UserDO  userDo = UserAuthContext.getUserInfo();

总结

总的来说ThreadLocal是线程安全的,所以使用起来很方便,不需要考虑共享变量产生的安全问题了。其次ThreadLocal已经封装好了基础的使用方法,所以代码相当简洁。最后我们在实战中看到,它很好的解决了上下文中数据传递的问题。

当然使用ThreadLocal也是有风险的,可能会存在内存泄露,这是因为他在Entry中存储是弱引用的,但对应的值是强引用,也就是说当ThreadLocal对象被回收后,其对应的值可能仍然存在于ThreadLocalMap中,无法被垃圾回收器回收。如果线程长时间运行,不断创建和使用ThreadLocal,就可能会占用大量内存,最终导致OutOfMemoryError。

所以在使用ThreadLocal的时候,上方我提到的remove方法一定要设计合理。否则有可能存在值覆盖及泄露问题。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ThreadLocal是什么?
  • 来一个简单的demo
  • 通过get看看内部结构
    • get方法
    • 初始化值
  • set方法
  • 实战
    • 创建用户授权上下文工具
    • 拦截器验证授权并在上下文赋值用户信息
    • 使用用户信息
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档