大厂是怎么用ThreadLocal?ThreadLocal核心原理分析

简介: ThreadLocal**是Java中的一个线程本地变量类。它可以让每个线程都有自己独立的变量副本,而不会相互影响。

介绍

ThreadLocal是Java中的一个线程本地变量类。它可以让每个线程都有自己独立的变量副本,而不会相互影响。

  • 在多线程编程中,线程共享同一个变量可能会带来并发访问的问题。使用ThreadLocal可以解决这个问题,使得每个线程都能够拥有自己独立的变量,实现线程隔离。
  • 同时可以利用ThreadLocal跨方法传递变量,可以减少代码的入侵更改,在项目公共组件设计架构中也是一个不错的选择。

ThreadLocal的使用很简单,其中主要有三个方法

  • set(obj) :设置需要存储的值
  • get() :获取值
  • remove() :移除值,此操作很有必要,否则会造成内存泄漏

源码解读

对于ThreadLocal的使用想必大家都了解,但是究竟是怎么设置值、为什么在当前线程中可以获取到设置的值,它是怎么存储的,为什么使用时大家都说会有内存泄漏的隐患呢?
接下来可以带着这些疑惑来来从源码角度分析。

核心源码

  • Thread类

Thread类中维护ThreadLocal.ThreadLocalMap属性,用于存储多个当前线程独有的本地变量值;

ThreadLocalMap属性的初始化是在调用ThreadLocal的set(val)方法中完成的

ThreadLocalMap属性获取存储于的变量值是在调用ThreadLocal的get()方法中执行的

public class Thread implements Runnable {
   
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
  • ThreadLocal类

严格来讲,ThreadLocal类更像是一个工具类,使用它的set(val)方法可以给当前线程设置值,get()获取值,remove()来清除值

public class ThreadLocal<T> {
   

    // 用于给线程创建ThreadLocalMap对象
    protected void createMap(Thread t, T firstValue) {
   
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    public void set(T value) {
   
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 从当前线程中拿到ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
   
            // 直接设置值
            map.set(this, value);
        } else {
   
            // 创建ThreadLocalMap对象并设置值
            createMap(t, value);
        }
    }

    public T get() {
   
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        ...
        return result;
    }

    // ThreadLocalMap是由继承自WeakReference的Entry类型的数组来存储设置本地变量值的,Entry的k就是我们新建的ThreadLocal对象,值为需要存储的值
    static class ThreadLocalMap {
   
        private Entry[] table;
        static class Entry extends WeakReference<ThreadLocal<?>> {
   
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
   
                super(k);
                value = v;
            }
        }
        ...
    }
}

ThreadLocal.get()的执行过程

  1. Thread t = Thread.currentThread(); // 获取当前线程
  2. ThreadLocalMap map = getMap(t); // 从当前线程中获取ThreadLocalMap
  3. ThreadLocalMap.Entry e = map.getEntry(this); // 根据当前(ThreadLocal)this作为key获取ThreadLocalMap.Entry
  4. T result = (T)e.value;// 从Entry中取出存储的value值

使用ThreadLocal为什么会有内存泄漏的隐患呢?

说到底还是用弱引用导致的原因,Java 弱引用(WeakReference) 弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

当GC时,仅仅会把ThreadLocalMap.Entry中的用(WeakReference)修饰的key给回收掉,然而value还是会被ThreadLocalMap.Entry对象一直引用,导致无法回收;

所以,我们在使用ThreadLocal时要养成好的习惯,使用完之后一定要记得显示调用remove()方法去清除这个对象。

使用案例

案例一:全局用户Session设置

场景描述

用户登录后,会给客户端下发身份令牌Token,客户端每次请求服务端接口都会携带此Token来标识用户身份,
在此请求贯穿的整个线程生命周期中,我们在任何业务相关逻辑中都可以知道这个用户信息,从而可以记录操作日志等。

代码实现

  • 1 创建用于存储用户信息的ThreadLocal对象的上下文类
public class ApiUserContext {
   

    // 创建存储用户信息的ThreadLocal对象
    public static ThreadLocal<ApiUser> curUser = new ThreadLocal<>();

    // 返回当前用户ID
    public static String getCurUserId() {
   
        return getCurUser().getId();
    }

    // 返回当前用户
    public static ApiUser getCurUser() {
   
        return curUser.get();
    }
}
  • 2 通过实现HandlerInterceptor的preHandle方法拦截请求解析用户信息
public class JwtAuthCheckInterceptor implements HandlerInterceptor {
   

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   
        // 读取请求头的token
        String token = request.getHeader(ApiAuthConstant.TOKEN);
        // 解析校验JWT Token
        Claims claims = jwtClientKit.getTokenClaim(token);
        ApiUser authedUser = JacksonUtils.toJavaBean(claims.getSubject(), ApiUser.class);
        // 将用户信息存储在ThreadLocal中
        ApiUserContext.curUser.set(authedUser);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
   
        // 从ThreadLocal中删除变量
        ApiSecurityContext.curUser.remove();
    }
}
  • 3 业务方法中使用

    public class DemoBiz {
         
      public void accessApi() {
         
          ApiUser curUser = ApiUserContext.getCurUser();
          log.info("userId = {}, username = {}, 访问接口", curUser.getId(), curUser.getUsername());
      }
    }
    
  • Github详细代码

https://github.com/yeeevip/yeee-memo/blob/master/memo-parent/memo-common/common-auth/common-app-auth-client/src/main/java/vip/yeee/memo/common/appauth/client/interceptor/JwtAuthCheckInterceptor.java

案例二:统一支付之上下文切换

场景描述

对于ToB的支付系统中,需要根据用户所属的租户来获取商户支付配置去调用三方支付接口进行下单,这时可以通过ThreadLocal
设置当前用户请求的支付配置上下文,在调用三方支付接口时可以随时获取达到跨方法的透传

代码实现

  • 1 创建用于存储当前租户支付配置的ThreadLocal对象的上下文类
@Slf4j
public class PayContext {
   

    private final String lesseeId;
    private final PayProperties payProperties;
    private final WxPayConfigBO wxPayConfig;
    private final AliPayConfigBO aliPayConfig;
    private final static ThreadLocal<PayContext> PAY_CONTEXT_THREAD_LOCAL = new ThreadLocal<>();

    public PayContext(String lesseeId, PayProperties payProperties, WxPayConfigBO wxPayConfig, AliPayConfigBO aliPayConfig) {
   
        this.lesseeId = lesseeId;
        this.payProperties = payProperties;
        this.wxPayConfig = wxPayConfig;
        this.aliPayConfig = aliPayConfig;
    }
}
  • 2 编写初始化支付上下文方法
public class PayContext {
   
    public static void initContext(String lesseeId) {
   
        try {
   
            PayContext payContext = null;
            // 部分代码省略,判断是否为空,不为空才新建PayContext对象
            if (payContext == null) {
   
                PayChannelConfigService channelConfigService = (PayChannelConfigService) SpringContextUtils.getBean(PayChannelConfigService.class);
                PayProperties payProperties = (PayProperties) SpringContextUtils.getBean(PayProperties.class);
                WxPayConfigBO wxPayConfigBO = channelConfigService.getWxPayChannelConfig(lesseeId);
                AliPayConfigBO aliPayConfigBO = channelConfigService.getAliPayChannelConfig(lesseeId);
                payContext = new PayContext(lesseeId, payProperties, wxPayConfigBO, aliPayConfigBO);
            }
            // 将支付上下文对象PayContext放置到ThreadLocal中
            PAY_CONTEXT_THREAD_LOCAL.set(payContext);
        } catch (Exception e) {
   
            log.error("初始化支付上下文失败", e);
            throw new BizException("初始化支付上下文失败");
        }
    }
}
  • 3 业务方法中使用
public class UnifiedPayOrderService {
   

    public UnifiedOrderRespBO unifiedOrder(UnifiedOrderReqVO reqVO) throws Exception {
   
        try {
   
            // 初始化设置当前用户线程支付上下文
            PayContext.initContext(reqVO.getLesseeId());
            UnifiedOrderRespBO respBO = wxAppPayKit.unifiedOrder(reqBO);
            return respBO;
        } finally {
   
            // 清除ThreadLocal对象
            PayContext.clearContext();
        }
    }
}

public class WxAppPayKit extends BaseWxPayKit {
   

    @Override
    public UnifiedOrderRespBO unifiedOrder(UnifiedOrderReqBO reqBO) {
   
        try {
   
            // 通过PayContext从ThreadLocal中获取当前租户支付配置
            PayContext payContext = PayContext.getContext();
            Config config = new Config(payContext);
            wxPayService.setConfig(config);
            UnifiedOrderRespBO respBO = wxPayService.createOrder(reqBO);
            ... 
            ...
            return respBO;
        } catch (Exception e) {
   
            log.info("【统一下单-微信APP支付】- 下单失败 reqBO = {}", reqBO, e);
            throw new BizException(e.getMessage());
        }
    }
}
  • Github详细代码
https://github.com/yeeevip/yeee-memo/blob/master/third-sdk/third-pay/src/main/java/vip/yeee/memo/demo/thirdsdk/pay/paykit/PayContext.java

版权 本文为yeee.vip原创文章,转载无需和我联系,但请注明来自https://www.yeee.vip

目录
相关文章
|
6月前
|
存储 安全 Java
面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
字节面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
78 0
|
6月前
|
存储 前端开发 Java
深入剖析ThreadLocal使用场景、实现原理、设计思想
深入剖析ThreadLocal使用场景、实现原理、设计思想
深入剖析ThreadLocal使用场景、实现原理、设计思想
|
6月前
|
存储 安全 Java
ThreadLocal原理讲解
ThreadLocal原理讲解
56 0
|
存储 算法 安全
【多线程系列-05】深入理解ThreadLocal的底层原理和基本使用
【多线程系列-05】深入理解ThreadLocal的底层原理和基本使用
259 4
|
Java 数据库连接
ThreadLocal原理和实践
ThreadLocal是线程本地变量,解决多线程环境下成员变量共享存在的问题。ThreadLocal为每个线程创建独立的的变量副本,他的特性是该变量的引用对全局可见,但是其值只对当前线程可用,每个线程都将自己的值保存到这个变量中而各线程不受影响。
162 0
ThreadLocal原理和实践
|
存储 SpringCloudAlibaba Java
浅析ThreadLocal使用及实现原理
提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其`get` 或 `set`方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。`ThreadLocal`实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联 。所以ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以这么说Th
109 0
浅析ThreadLocal使用及实现原理
|
存储 Java 数据安全/隐私保护
ThreadLocal的实现原理&源码解析
ThreadLocal是Java中的一个线程封闭机制,它提供了一种线程局部变量的解决方案,可以使每个线程都拥有自己独立的变量副本,互不干扰。
93 0
|
存储 算法 Java
ThreadLocal原理解析
ThreadLocal原理解析
106 1
ThreadLocal原理解析
|
存储 安全 Java
|
Java 定位技术
ThreadLocal原理
经典八股文之ThreadLocal原理
190 0