ThreadLocal 原理解析

一篇学习笔记。

这里就不讲 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 对象
  1. 还没创建 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 对象(后面讲内存泄漏的时候会再提到这个)。

第一次创建的大致流程就是这样的。

  1. 获取到的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()方法可以有效的避免发生内存泄漏

相关推荐
z26373056112 小时前
springboot继承使用mybatis-plus举例相关配置,包括分页插件以及封装分页类
spring boot·后端·mybatis
追逐时光者5 小时前
分享一个纯净无广、原版操作系统、开发人员工具、服务器等资源免费下载的网站
后端·github
JavaPub-rodert6 小时前
golang 的 goroutine 和 channel
开发语言·后端·golang
ivygeek7 小时前
MCP:基于 Spring AI Mcp 实现 webmvc/webflux sse Mcp Server
spring boot·后端·mcp
GoGeekBaird8 小时前
69天探索操作系统-第54天:嵌入式操作系统内核设计 - 最小内核实现
后端·操作系统
鱼樱前端8 小时前
Java Jdbc相关知识点汇总
java·后端
canonical_entropy9 小时前
NopReport示例-动态Sheet和动态列
java·后端·excel
kkk哥9 小时前
基于springboot的母婴商城系统(018)
java·spring boot·后端
Asthenia041210 小时前
面试复盘:关于 Redis 如何实现分布式锁
后端