聊一聊 ThreadLocal

作者:余性笃厚

个签:努力登上我们所选择的舞台

一、ThreadLocal简介

ThreadLocal 是什么?看一下官方解释!

翻译 :这个类(ThreadLocal)提供线程局部变量/线程本地变量(Thread-local variables) 。ThreadLocal 变量与我们常见的变量不同,因为访问 ThreadLocal 变量(通过其get或set方法)的每个线程都有自己的、独立的变量副本。ThreadLocal 实例通常被定义为类中的私有静态属性(private static修饰),其使用目的是将状态(例如,用户ID或事务ID)与线程关联起来。

注意 :将 ThreadLocal 中的变量值在线程中称为变量副本,是因为每个线程都有自己的、独立的这个值。简单来说,变量值实际存储在当前线程的 ThreadLocalMap 实例中,而非 ThreadLocal 实例中。因此,ThreadLocal 是线程隔离/线程独有,线程和线程之间不会发生冲突。

API介绍

  • get():获取当前线程的 ThreadLocal 变量的值。如果在当前线程中该变量尚未被设置,那么将返回 null
  • set(T value):设置当前线程的 ThreadLocal 变量值
  • remove():移除当前线程的 ThreadLocal 变量和变量值
  • initialValue() :这个方法是一个 protected 的方法,它通常在子类中被重写,以提供默认的初始值(可以忽略,从 Java 8 开始,推荐使用 withInitial() 方法来代替它)
  • withInitial(Supplier<? extends T> supplier):这是个静态方法,使用指定的 Supplier 函数式接口提供的初始值创建一个 ThreadLocal 实例

简单使用示例

java 复制代码
public static void main(String[] args) {
    ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
    threadLocal1.set(1);
    System.out.println(threadLocal1.get()); // 1
    threadLocal1.remove();
    System.out.println(threadLocal1.get()); // null
    
    // 下面两个等价
    ThreadLocal<Integer> threadLocal2 = ThreadLocal.withInitial(() -> 2);
    System.out.println(threadLocal2.get()); // 2
    
    ThreadLocal<Integer> threadLocal3 = ThreadLocal.withInitial(new Supplier<Integer>() {
        @Override
        public Integer get() {
            return 2;
        }
    });
    System.out.println(threadLocal3.get()); // 2
}

二、ThreadLocal源码分析

Thread、ThreadLocal、ThreadLocalMap的关系

  • Thread 与 ThreadLocalMap 是 has a 的关系。初始时,Thread 中的 threadLocals 为空,只有在当前线程中创建了 ThreadLocal 变量并且设置了变量值,才会创建 ThreadLocalMap 实例(类似单例的懒汉式
  • ThreadLocal 类中定义了静态内部类 ThreadLocalMap,把它当作伪 Map 即可(类似于常见的Map),就是存储key-value 键值对
  • ThreadLocalMap 类中又定义了 Entry 静态内部类,该类定义了一个 Entry 类型的数组 table。为什么要自定义一个 Entry 呢,因为现有不满足要定制,Entry 的 k 为 ThreaLocal 实例,v 为变量值(ThreadLocal< Integer > tl = new ThreadLocal<>() 这段代码中 tl 就是 k,Integer 实例就是 v);为什么要定义一个 table:Entry[] (下图左下角),因为一个 ThreadLocal 实例就对应一个 Entry,一个线程可能创建多个 ThreadLocal 实例,就有多个 Entry,因此定义了一个 Entry 类型的数组。并且发现,其中 k 为弱引用,在没有强引用的情况下,经可达性分析算法发现当前对象不可达,经一次 GC 之后 k 就为 null,这个后面详细讲解

get()、set()、remove()、withInitial()

  • set(T): 当线程中首次创建 ThreadLocal 实例,并且调用 set() 方法时,通过 ThreadLocal 创建 ThreadLocalMap 实例,赋值给当前线程中的 threadLocals,然后创建 Entry 对象,将当前 ThreadLocal 实例作为 Entry 的 k,将变量值作为 v。继续 set() 时,先获取当前线程 ThreadLocalMap 实例,它是非空的,如果存在 Entry 的 k 等于当前 ThreadLocal 的引用,那么会进行 value 覆盖,否则就 new Entry(key, value)
  • get(): 获取变量值的过程和 set() 源码很相似,如果当前线程 ThreadLocalMap 为空,或没有与当前 ThreadLocal 引用匹配的 Entry,那么将返回 null,否则,返回变量值(注意如果存在哈希冲突,ThreadLocal 中解决冲突的方式是线性探测法,需要遍历 table:Entry[],判断所有的 Entry)
  • remove(): e.get() 作用是将 Entry 的 k 置空,然后 expungeStaleEntry() 将 k 等于空的 Entry 的 v 置空,Entry 所在的 table:Entry[] 索引位置也置空,这样就相当于 清除了 Entry
  • withInitial(): 此方法方法是 ThreadLocal 类的一个静态工厂方法,用于创建 ThreadLocal 实例,并通过传入的 Supplier 实现类对象 提供每个线程的初始值。这个方法的主要目的是提供一种延迟初始化的机制,只有在首次调用 get() 方法时,才会使用 Supplier 提供的逻辑初始化 ThreadLocal 的值。仔细分析下,SuppliedThreadLocal 继承了 ThreadLocal,就继承了 ThreadLocal 中的 get() 方法,同时重写了 initialValue(),这个方法的作用就是提供变量值,并且只在第一次 个体() 调用,最终变量值还是生成变量副本,存储在 ThreadLocalMap 中;
    看一下首次调用 ThreadLocal 实例的 get() 的流程:首次调用由 withInitial() 创建的 ThreadLocal 变量的 get() 方法 -> setInitialValue() -> initialValue() -> Supplier 实现类的 get() -> 获取变量值 -> 设置到 ThreadLocalMap。

三、ThreadLocal 业务逻辑异常和内存泄露

看一下来自阿里巴巴的Java开发手册(黄山版)中关于 ThreadLocal 使用规范的强制要求。

  • 可能影响后续业务逻辑异常的问题
    举例1:我们知道 Tomcat 是一个流行的 Java Web 服务器,它使用线程池来处理客户端请求,就存在线程复用,假如 A 用户认证登录成功,Tomcat 分配了线程,则工具 Bean 中与 ThreadLocal 变量映射的是 A用户ID,即当前线程存储变量副本 A用户ID,业务 Bean 通过工具 Bean 获取当前线程的变量副本,一系列操作后,在业务结束前并未调用 remove() 方法去清除 ThreadLocal 实例。假如此时 B 用户认证登录,由于线程复用,可能又分配了上一次分配给 A 用户的那个线程,又由于 ThreadLocal 属性通常定义为静态的,因此,如果 B 用户对应的业务中获取该变量副本没有加以判断,直接使用,那么可能存在业务逻辑异常(在 B用户 的业务中操作 A用户ID)。

    举例2:再比如在微服务架构的认证登录中,微服务和微服务之间进行 Feign 调用,上游微服务经常的操作是将用户ID存入 HTTP 请求头,下游微服务通过拦截器等,从 HTTP 的请求头获取用户ID存入 ThreadLocal 变量,另一个拦截器判断 ThreadLocal 变量是否为空(用户是否登录),如果在线程复用场景,没有合理进行 remove() 那么可能有业务逻辑异常
    注意: 如果你真理解了会发现,这两个案例场景其实就算不调用 remove() 也不出现后续业务逻辑异常问题,但是这个可能存在业务异常问题的意思是表达清楚了。

  • 内存泄露:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露
    下图,Entry 继承了 WeakReference<ThreadLocal<?>>,这意味着 Entry 对象会持有一个对 ThreadLocal 对象的弱引用,当没有强引用指向 ThreadLocal 实例,在 GC 后,ThreadLocal 实例将被回收,Entry 的 k 会等于 null,上面已经研究了 get() 代码,如果连 ThreadLocal 引用都没有了,那么 ThreadLocal 实例关联的值(即 v)就压根无法获取,但 Entry 和 关联的值却又存在内存中,这就是内存泄露。细心的你肯定发现了,在上面源码分析中有几个方法是专门处理这个问题,expungeStaleEntry()、replaceStaleEntry()、cleanSomeSlots(),这些方法被设计在一些条件下(主动调用、符合一定判断)清理掉这些已经不再需要的 Entry 对象和关联的值,这些方法只能称作适当补偿,并不是说每次调用都会全部清除 table:Entry[] 中所有无法访问的 Entry。因此最佳实践表示,正确使用 ThreadLocal 时,最好在不再需要的时候手动调用 remove() 方法来清理资源,以避免潜在的内存泄漏。

四、小总结

  • 每个 Thread 对象维护着一个 ThreadLocalMap 的引用 - ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储 - 调用 ThreadLocal 的 set() 方法时,实际上就是往 ThreadLocalMap 设置值,k 是 ThreadLocal对象,值 v 是传递进来的对象 - 调用 ThreadLocal 的 get() 方法时,实际上就是往 ThreadLocalMap 获取值,k 是ThreadLocal 对象,v 就是获取的变量值 - 调用 ThreadLocal 的 withInitial() 方法,用来创建 ThreadLocal 变量和设置初始值,首次调用此 ThreadLocal 实例的 get() 方法,从 Supplier 子类对象获取变量值,然后往 ThreadLocalMap 设置值 - ThreadLocal 本身并不存储值,它只是自己作为一个 key 来让线程从 ThreadLocalMap 获取 value,正因为这个原理,所以 ThreadLocal 能够实现 "数据隔离" - ThreadLocal 并不解决线程间共享数据的问题,ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景 - ThreadLocal 通过隐式的在不同线程内创建独立变量副本避免了实例线程安全的问题 - 每个线程持有一个只属于自己的专属 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题 - ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题 - 通过 expungeStaleEntry、cleanSomeSlots、replaceStaleEntry 这三个方法回收键为 null 的 Entry 对象以及变量值,从而防止内存泄漏,属于安全加固的方法

五、强引用、软引用、弱引用、虚引用分别是什么?

在 Java 中,Reference 类及其子类是用于实现引用对象的基类。Reference 类主要有三个子类:SoftReferenceWeakReference、和 PhantomReference,它们分别代表软引用、弱引用和虚引用。

  • 强引用(默认支持模式): 当内存不足,JVM 开始垃圾回收,对于强引用的对象,就算是出现了 OOM(Out of Memory 主要指堆区内存不足) ,该对象也不会被回收,强引用是我们经常写的普通对象引用,把一个对象赋给一个引用变量(在 C/C++ 中的名词称为指针变量),这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,因此强引用是造成 Java 内存泄漏的主要原因之一。ThreadLocal 内存泄露主要就是因为 Entry 和 ThreadLocal 关联的对象是强引用
  • 软引用: 软引用是一种相对强引用弱化了一些的引用,通过java.lang.ref.SoftReference类来实现对于只有软引用的对象来说,只有当系统内存不足时它会被回收。软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收
  • 弱引用: 它比软引用的生存期更短,通过 java.lang.ref.WeakReference 类来实现。当一个对象只被弱引用引用时,在下一次垃圾回收(GC)时,即使内存空间足够,也会被回收
  • 虚引用: 形同虚设,虚引用并不会决定对象的生命周期,通过java.lang.ref.PhantomReference类来实现。1.如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收(PhantomReference 的 get 方法 总是返回 null),2.它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用,3.虚引用的主要作用是跟踪对象被垃圾回收的状态以及引用对象被放入与之关联的引用队列中,允许程序员在适当的时机进行一些额外的操作。 仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。其意义在于:说明一个对象已经进入 finalization 阶段,可以被 GC 回收,用来实现比 finalization 机制更灵活的回收操作
相关推荐
侠客行03175 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪5 小时前
深入浅出LangChain4J
java·langchain·llm
老毛肚6 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎7 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
Yvonne爱编码7 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚7 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂7 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
fuquxiaoguang7 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析
琹箐8 小时前
最大堆和最小堆 实现思路
java·开发语言·算法
__WanG8 小时前
JavaTuples 库分析
java