大厂是怎么用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

目录
相关文章
|
SQL 关系型数据库 数据库
学习分布式事务Seata看这一篇就够了,建议收藏
学习分布式事务Seata看这一篇就够了,建议收藏
16633 2
Java Exception异常信息怎么打印、记录,几种方式自己选
Java Exception异常信息怎么打印、记录,几种方式自己选
893 0
Java Exception异常信息怎么打印、记录,几种方式自己选
|
定位技术
百度地图拾取经纬度转为标准GEOJSON格式的函数解决方案
百度地图拾取经纬度转为标准GEOJSON格式的函数解决方案
384 0
|
4月前
|
设计模式 存储 安全
并发设计模式实战系列(7):Thread Local Storage (TLS)
🌟 大家好,我是摘星! 🌟今天为大家带来的是并发设计模式实战系列,第七章Thread Local Storage (TLS),废话不多说直接开始~
135 0
|
5月前
|
存储 缓存 安全
【Java并发】【ThreadLocal】适合初学体质的ThreadLocal
ThreadLocal 是 Java 中用于实现线程本地存储(Thread-Local Storage)的核心类,它允许每个线程拥有自己独立的变量副本,从而在多线程环境中实现线程隔离,避免共享变量带来的线程安全问题。
113 9
【Java并发】【ThreadLocal】适合初学体质的ThreadLocal
|
10月前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
监控 安全 Java
Java多线程调试技巧:如何定位和解决线程安全问题
Java多线程调试技巧:如何定位和解决线程安全问题
260 2
|
机器学习/深度学习 人工智能 算法
图解机器学习 | 朴素贝叶斯算法详解
朴素贝叶斯是一个非常直观的模型。本文讲解朴素贝叶斯算法的核心思想、贝叶斯公式、条件独立假设、平滑出等重要知识点,并图解多项式贝叶斯和伯努利贝叶斯等多种形态。
2186 1
图解机器学习 | 朴素贝叶斯算法详解
|
Java Spring
【Azure Developer】Springboot 集成 中国区的Key Vault 报错 AADSTS90002: Tenant 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' not found
【Azure Developer】Springboot 集成 中国区的Key Vault 报错 AADSTS90002: Tenant 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' not found
203 0
|
JavaScript Ubuntu 应用服务中间件
nginx扩展 OpenResty 实现防cc攻击教程
使用OpenResty实现CC攻击防护,包括两个主要步骤:限制请求速度和JS验证。首先,安装依赖(RHEL/CentOS需安装readline-devel, pcre-devel, openssl-devel,Ubuntu需安装libreadline-dev等)。然后,安装Luajit和OpenResty。在Nginx配置中,创建`lua`共享字典并设置`content_by_lua_file`调用lua脚本。lua脚本检查请求频率,超过限制则返回503,否则增加计数。同时,通过JS验证,生成随机码并重定向用户,用户需携带正确验证码请求才能访问。
364 0