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

2023-08-27 16:28:11 浏览数 (1)

介绍

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

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

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

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

源码解读

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

核心源码

  • Thread类

Thread类中维护ThreadLocal.ThreadLocalMap属性,用于存储多个当前线程独有的本地变量值; ThreadLocalMap属性的初始化是在调用ThreadLocal的set(val)方法中完成的 ThreadLocalMap属性获取存储于的变量值是在调用ThreadLocal的get()方法中执行的

代码语言:javascript复制
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
  • ThreadLocal类

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

代码语言:javascript复制
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对象的上下文类
代码语言:javascript复制
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方法拦截请求解析用户信息
代码语言:javascript复制
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 业务方法中使用
代码语言:javascript复制
public class DemoBiz {
    public void accessApi() {
        ApiUser curUser = ApiUserContext.getCurUser();
        log.info("userId = {}, username = {}, 访问接口", curUser.getId(), curUser.getUsername());
    }
}
  • Github详细代码
代码语言:javascript复制
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对象的上下文类
代码语言:javascript复制
@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 编写初始化支付上下文方法
代码语言:javascript复制
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 业务方法中使用
代码语言:javascript复制
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详细代码
代码语言:javascript复制
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

0 人点赞