一篇学习笔记。
这里就不讲 ThreadLocal 的基本使用了,主要来聊一聊它的原理。
一、ThreadLocal 概述
ThreadLocal 是 Java 多线程编程中用于实现线程私有变量的核心工具类。它通过为每个线程创建独立的变量副本,解决了多线程环境下共享变量引发的线程安全问题。其经典应用场景包括:
- 数据库连接管理(每个线程独立使用 Connection)
- 用户会话信息存储(如 Spring 的
RequestContextHolder
) - 避免参数透传(如分布式链路追踪的上下文传递)
二、核心实现原理
ThreadLocal 的实现主要有三个类参与,分别是 Thread,ThreadLocal,ThreadLocalMap,实际的数据就是存储在 ThreadLocalMap 中的 Entry 对象中(Entry是ThreadLocalMap的静态内部类,继承了 WeakReference,会使用弱引用在一定层度上避免内存泄漏。)。
- Thread: Java中的线程类,用于创建一个新的线程,其中会有 ThreadLocalMap 类型的一个属性(后面会讲)
- ThreadLocal: Java 多线程编程中用于实现线程私有变量的核心工具类。
- ThreadLocalMap: ThreadLocal 中的静态内部类。
这里先给出这三个类的一个关系图,后续的讲解都在这个图上来进行说明的,会通过源码来进行证明。(早期的实现不是这个样子的,这里就不做特别说明了,减少学习成本)
从这个图来进行分析,每一个 Thread 对象(可以理解为一个线程),都有一个 ThreadLocalMap 的属性,当我们使用 ThreadLocal 的set()
方法来保存用户变量的时候,就会把当前的 ThreadLocal 对象作为 key,需要保存的用户变量作为 value 存到这个 ThreadLocalMap 中 。
因为每个线程的成员变量是线程私有的,所以解决了多线程环境下共享变量引发的线程安全问题。
ThreadLocalMap 的成员变量和线程绑定,所以除了方法的参数以外,也可以通过 ThreadLocal 来进行数据的传递。
Thread
我们先来看看 Thread 中的实现:
这是我在 Thread 源码中截的图,可以确实 Thread 维护一个 ThreadLocalMap 的成员属性 threadLocals,也可以看出来这个 ThreadLocal的一个静态内部类。
这个上面有一段注释,翻译过来大概意思是:
ThreadLocal 值,此映射由 ThreadLocal 类维护 。
就是在说,Thread 的对象不会对这个属性进行操作,当时使用 ThreadLocal 的API的时候,会来操作这个属性。
ThreadLocal
接下来,来看一下 ThreadLocal 是怎么来维护 Thread 中的 threadLocals 成员属性的。
我们在使用 ThreadLocal 的时候,一般都是用 set()
和 get()
方法,我们就来着重分析一下这两个方法。
这里先给出set()
源代码截图再来做说明。
从图中可以看到 set()
方法的第一行Thread t = Thread.currentThread()
获取到了当前线程,第二行ThreadLocalMap map = getMap(t)
,是调用了一个getMap()
的方法,并把当前线程作为参数传入,获取到一个 ThreadLocalMap 对象,我们跟进这个方法:
从这个方法中可以看出来,返回的 ThreadLocalMap 对象是前面我们提到的 Thread 的一个成员属性。 我们再顺着set()
方法往下走,这个时候就有两种情况:
- 第一次访问,还没创建 ThreadLocalMap 对象,这个时候获取到的就是
null
- 已经创建好 ThreadLocalMap 对象
- 还没创建 ThreadLocalMap 对象
我们先来看看第一种情况,也就是获取到的 map == null 的时候,也就是 else 这部分的代码块,这个时候就会执行 createMap(t, value)
这行代码,把当前线程和传进来的值作为参数,我们接着跟进这个方法,看看做了些什么。
这里就是调用构造方法创建了一个 ThreadLocalMap 对象并复制给当前线程的 threadLocals 属性,要注意,这里构造方法传入的 this 是当前调用set()
方法的 ThreadLocal 对象,再跟进这个方法。
可以看到第一行创建了一个 Entry 类型的数组,这里传入了初始的容量(初始容量是16,我就不再证明了),并把创建好的数组赋值给了 table
,这个 table
是 ThreadLocalMap 的一个成员属性,就是一个 Entry 数组,第二行代码就是把传入的 ThreadLocal 对象就行 hash 运算,算出一个数组下标,主要来看一下这个 Entry 创建的过程,ThreadLocal 就是在这里使用的弱引用 ,后面两行不用过多的在意。
Entry 的构造方法传入了我们的 ThreadLoacl 对象和要设置的 value,我们来看一下这个 Entry 的构造方法做了些什么,前面也说了, Entry 继承了 Java 中的弱引用,就是在这里使用了
这里做一个简单的说明,如果一个对象只有弱引用指向它,垃圾回收器工作的时候会回收这个对象(如果还有强引用指向这个对象,垃圾回收器是不会回收的)
从图中可以看到, Rntry 继承了 WeakReference<ThreadLocal<?>>
,这个是Java中的弱引用对象,通过它的构造函数传入的对象会通过弱引用连接,在构造方法中,把 k 传给了父类的构造方法,也就是说,使用弱引用指向了我们的 ThreadLocal 对象(后面讲内存泄漏的时候会再提到这个)。
第一次创建的大致流程就是这样的。
- 获取到的map不为空
这里就简单说一下了,流程和第一次创建的时候是差不多的,不过会多一些检查判断。
它会进行 hash 运算找到对应的槽位,如果创建了就赋值,当然了,会进行一些健壮性判断,如果没有的话,就会 new 一个新的 Entry 对象。
我们再来看一下get()
方法的源码截图:
这里也是先获取当前线程的对象,然后判断是否有 map 对象和是否有 对应的 Entry 对象,没有的话,会调用一个获取初始值的方法,并创建新的 Entry 在这个 map 中,代码和上面的 set()
差不多,我就不再仔细分析了。
三、内存泄漏
- 内存泄漏:程序中已经动态分配的内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至崩溃。此外内存泄漏的堆积最终也会导致内存溢出
这里有一个内存结构图,在栈区中有ThreadLocal对象和当前线程对象,分别指向堆区真正存储的类对象,这俩个指向都是强引用。在堆区中当前线程肯定是只有自己的Map的信息的,而Map中又存储着一个个的Entry节点;在Entry节点中每一个Key都是ThreadLocal的实例,同时Value又指向了真正的存储的数据位置,以上便是下图的引用关系。
内存泄漏出现的原因
内存泄漏,其实就是指的Entry这块内存不能正确释放
先说 Entry 的 key:
如果 key 使用的是强引用 当我们在业务代码中使用完ThreadLocal,在栈区指向堆区的这个指向关系就会被回收掉了,但是由于Key是强引用指向ThreadLocal,故而堆区中的ThreadLocal无法被回收,此时的Key指向ThreadLocal,另外由于当前线程还没有结束,则下面那条强引用指向关系任然存在(使用线程池)。这个THreadLocal对象就不会被回收。
然后就是是弱引用的情况,当我们在业务代码中使用完ThreadLocal就通过垃圾回收(GC)进行了回收,那么由于Key是弱引用,Key此时就指向null,ThreadLocal对象就会被回收。
注意:
这两种情况都没有手动的调用remove()
方法,所以他们的 value 都还存在,没有被回收,依然存在内存泄漏问题(前提是线程还在运行,使用线程池容易发生这种情况)。
所以我们在写代码的时候记得写remove()
方法可以有效的避免发生内存泄漏