ThreadLocal 详解及使用

ThreadLocal 详解及使用

简介

被用作在同一个线程中存取信息的对象;该对象存储的信息仅在该线程中可以访问,做到了线程间隔离。

常用在当请求进入拦截器时,在该请求线程中存入一些信息(如根据用户请求确定其用户的一些基本信息并存入,方便该线程的方法随时取出利用),在请求结束时调用其 remove() 方法清空信息。

实现原理简介

结构

每个 Thread 内都会个 ThreadLocal.ThreadLocalMap threadLocals = null 对象

public class Thread implements Runnable {
       
        private static native void registerNatives();
        static {
            registerNatives();
        }
    // ...
    
        ThreadLocal.ThreadLocalMap threadLocals = null;
    
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    // ...
    }

ThreadLocalMapThreadLocal 的静态内部类,也就意味着这个 ThreadLocalMap 是应用中公用的。
ThreadLocalMap 内部存储结构类似于 Map.Entry<K,V> ,而这个 Entry 结构的 keyWeakReference<ThreadLocal<?>> ,这也是会发生内存泄漏的原因(后文解释)

public class ThreadLocal<T> {
    // ...
    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        private static final int INITIAL_CAPACITY = 16;

        private Entry[] table;
        
        // ...
    }
初始化时机

它会在当前线程第一次调用的时候初始化(当 threadlocals == null

// 初始化
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

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

实际操作是在每次调用 ThreadLocal 方法时都会传入当前的线程,根据这个线程找到这个线程对应的ThreadLocalMap ,而ThreadLocalMap 内的 Entry<ThreadLocal,Object> 属性,这就是用来存储这个线程公用的 key,value。也就是说 ThreadLocal 是操作 ThreadLocalMap 的桥梁。

每个 ThreadLocalMap 都是这个线程独有的,所以是线程隔离的。

// get方法示例
public T get() {
    Thread t = Thread.currentThread();
    // 传入当前线程作参数,得到对应的 ThreadLocal
    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;
        }
    }
    return setInitialValue();
}

使用示例

  1. 定义存储信息类
@Data
public class ThreadLocalUser {

    /**
     * 用户id
     */
    private Integer userId;

    /**
     * 用户昵称
     */
    private String nickname;

    /**
     * 用户头像地址
     */
    private String avatarUrl;

    /**
     * 用户权限集合
     */
    private List<String> authorities;

}
  1. 定义操作该存储信息对象的 ThreadLocal
/**
 * ThreadLocal用户信息管理对象
 */
@Getter
@Setter
public class RequestThreadHolder {

    private final static String USERID = "userId";

    private final static String NICKNAME = "nickname";

    private final static String ROLES = "authorities";

    private final static ThreadLocal<ThreadLocalUser> THREAD_LOCAL = ThreadLocal.withInitial(ThreadLocalUser::new);

    /**
     * 根据请求的jwt解析用户信息初始化请求的用户信息
     * @param jwt
     */
    public static void init(String jwt){
        DecodedJWT decode = JWT.decode(jwt);
        Map<String, Claim> claims = decode.getClaims();
        Integer userId = Objects.isNull(claims.get(USERID)) ? 0 : claims.get(USERID).asInt();
        String nickname = StringUtils.isEmpty(claims.get(NICKNAME).asString()) ? "佚名" : claims.get(NICKNAME).asString();
        List<String> roles = Objects.isNull(claims.get(ROLES)) ? Collections.emptyList() : claims.get(ROLES).asList(String.class);
        THREAD_LOCAL.get().setUserId(userId);
        THREAD_LOCAL.get().setNickname(nickname);
        THREAD_LOCAL.get().setAuthorities(roles);
    }

    /**
     * 获取请求用户信息
     * @return
     */
    public static ThreadLocalUser getUserInfo(){
        return THREAD_LOCAL.get();
    }

    /**
     * 获取请求用户id
     * @return
     */
    public static Integer getLocalUserId(){
        return THREAD_LOCAL.get().getUserId();
    }

    /**
     * 获取请求用户的昵称
     * @return
     */
    public static String getLocalUserNickname(){
        return THREAD_LOCAL.get().getNickname();
    }

    /**
     * 获取请求用户角色列表
     * @return
     */
    public static List<String> getLocalUserRoles(){
        return THREAD_LOCAL.get().getAuthorities();
    }

    /**
     * 清除ThreadLocal信息
     */
    public static void clean(){
        THREAD_LOCAL.remove();
    }
}
  1. 拦截器编写
/**
 * 请求用户信息拦截器
 */
public class RequestThreadHolderInterceptor extends HandlerInterceptorAdapter {

    /**
     * 请求进入前向 ThreadLocal 注入用户信息
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
        String jwt = StringUtils.substringAfter(bearerToken,TokenConstant.TOKEN_PREFIX);
        RequestThreadHolder.init(jwt);
        if (StringUtils.isEmpty(jwt)){
            throw new InnerException("token不存在");
        }
        return super.preHandle(request, response, handler);
    }

    /**
     * 请求结束后调用清理 ThreadLocal
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        RequestThreadHolder.clean();
        super.afterCompletion(request, response, handler, ex);
    }
}
  1. 加入到拦截器
@Configuration
public class InterceptorConfigurer implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RequestThreadHolderInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/swagger-resources/**", "/v2/**", "/test/**");
    }
}

ThreadLocal 的内存泄漏问题

内存泄漏:内存中存在没有被程序使用的对象,但是无法被垃圾回收器回收。

ThreadLocal 的内存泄漏涉及到 Java 的弱引用。

JAVA引用类型
弱引用/软引用解释:
	对于以下的各类引用,new出来的只是对传入进去的对象声明一个弱/软引用,当这个对象还存在强引用时,即使显示调用System.gc()也无法回收该对象。
	
	public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        WeakReference<Object> weak2 = new WeakReference<>(object);
        System.out.println("weak:" + weak2.get());	//输出object对象
        object = null;
        System.gc();
    	//调用gc:输出null;否则输出object对象。
    	//因为若不执行object=null,则new object()这个对象一直存在强引用,无法回收。
        System.out.println("weak gc:" + weak2.get());
    }
1.强引用:
	强引用是使用最普遍的引用。如:Object obj = new Object();
	这时对这个Object对象就存在一个强引用。对于强引用如果一直存在,GC不会对其进行回收,即使内存溢出。所以如果对象确定不在使用,需要及时弱化,如:obj = null;

2.软引用(SoftReference):
	一个对象若只有软引用,则内存空间充足时,GC就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
	String str = new String("abc");
	//声明一个软引用对象,引用的是new String("abc")
    SoftReference<String> softReference = new SoftReference<String>(str);

3.弱引用(WeakReference)//对于弱引用,GC不管当前内存空间足够与否,都会回收它的内存。
	String str = new String("abc");
    WeakReference<String> weakReference = new WeakReference<>(str);
    
4.虚引用(PhantomReference):
	如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
示例

弱引用gc简单测试

public static void main(String[] args) {
    User user1 = new User();
    user1.setUsername("张三");
    User user2 = new User();
    user2.setUsername("李四");
    WeakReference<User> weak1 = new WeakReference<>(user1);
    WeakReference<User> weak2 = new WeakReference<>(user2);
    HashMap<WeakReference<User>, Integer> map = new HashMap<>();
    map.put(weak1,1);
    map.put(weak2,2);
    user1 = null;
    System.gc();
    for (Map.Entry entry:map.entrySet()){
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
}

image-20220807175428335

ThreadLocal 内存泄漏分析
  1. 构建弱引用对象

    @Getter
    @Setter
    // 继承弱引用对象,并指定泛型(非必须)
    public class WeakRefUser extends WeakReference<User> {
    
        private String name;
    
        // WeakReference的构造必须初始化referent属性,即标明这个对象是弱引用对象
        public WeakRefUser(User referent,String name) {
            super(referent);
            this.name = name;
        }
    }
    
  2. 代码测试

    public static void main(String[] args) {
        WeakRefUser weakRefUser1 = new WeakRefUser(new User(), "张三");
        WeakRefUser weakRefUser2 = new WeakRefUser(new User(), "李四");
        HashMap<WeakRefUser, Integer> map = new HashMap<>();
        map.put(weakRefUser1,1);
        map.put(weakRefUser2,2);
        System.gc();
        System.out.println();
    }
    

image-20220807180255665

image-20220807180421077

  1. ThreadLocal 内部构造查看

    ThreadLocal 的方法实际是操作其内部属性 ThreadLocalMap;而 ThreadLocalMap 的主要结构其实是一个弱引用的 Entry

static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {

            Object value;

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

        private static final int INITIAL_CAPACITY = 16;
    // ...
}

看到上述结构就能明白,平时我们操作的 Map 结构就是一个 Entry<K,V> ;而 ThreadLocalMapEntrykey 是弱引用的 referent 属性,这个属性只要经过 gc 就会被回收,使其指向 null ;这就会导致这个 Entry 中存在 keynull 的但其 EntryThreadLocalMap(静态类) 存在强关联,使得这样的 Entry 无法被 gc 回收,而这种元素又是无法被利用的,如果不及时清理,使这种类型元素大量堆积,就出现常说的内存泄漏(存在大量无法引用的对象)

  1. 避免 ThreadLocal 内存泄漏

    ThreadLocal 在调用 get()set()remove() 时都会处理这些为 nullkey ,但依旧建议在使用完及时 remove() 来保证一定移除这个 Entry

ThreadLocal 的衍生类

  • InheritableThreadLocal
    ThreadLoca l区别是 可以传递值给子线程。

    存在的问题:

    1. 线程不安全:如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量。
    2. 线程池中可能失效:在使用线程池的时候,在执行异步任务时可能不需要创建新的线程了(即子线程),因此也就不会再传递父线程的ThreadLocalMap 给子线程了。
  • TransmittableThreadLocal
    阿里开源的,很好的解决 InheritableThreadLocal 在线程池情况下,父子线程值传递问题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocal 是 Java 中的一个线程局部变量,它为每个线程提供了独立的变量副本,每个线程都可以通过 ThreadLocal 对象来访问自己的变量副本,而不会影响其他线程的副本。ThreadLocal 在多线程编程中非常有用,可以解决线程安全问题。 使用 ThreadLocal 的主要场景包括: 1. 线程上下文信息传递:有些情况下,我们需要在多个方法之间传递一些上下文信息,例如用户认证信息、数据库连接等。使用 ThreadLocal 可以避免在方法参数中传递这些上下文信息,每个线程都可以独立地访问自己的上下文信息。 2. 线程安全的对象:有些对象在多线程环境下是不安全的,如果每个线程都持有一个对象的副本,就可以避免多线程竞争访问导致的安全问题。例如 SimpleDateFormat 是非线程安全的,可以使用 ThreadLocal 来为每个线程提供一个独立的 SimpleDateFormat 对象。 3. 隐式参数传递:有些方法需要依赖某些参数,但是这些参数对于调用方来说并不是必须的。使用 ThreadLocal 可以将这些参数设置为 ThreadLocal 变量,在方法中直接获取这些参数,而不需要显式传递。 需要注意的是,使用 ThreadLocal 时要及时清理资源,避免内存泄漏。在使用完毕后,应该调用 ThreadLocal 的 remove() 方法来清理当前线程持有的变量副本。 总之,ThreadLocal 可以用于在多线程环境下实现线程安全、线程间数据隔离和传递上下文信息等功能。但是过度使用 ThreadLocal 也会导致代码可读性变差,增加了代码的复杂性,需要根据具体场景进行合理使用

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值