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

相关推荐
yuuki23323313 分钟前
【C语言】文件操作(附源码与图片)
c语言·后端
IT_陈寒17 分钟前
Python+AI实战:用LangChain构建智能问答系统的5个核心技巧
前端·人工智能·后端
无名之辈J41 分钟前
系统崩溃(OOM)
后端
码农刚子1 小时前
ASP.NET Core Blazor简介和快速入门 二(组件基础)
javascript·后端
间彧1 小时前
Java ConcurrentHashMap如何合理指定初始容量
后端
catchadmin1 小时前
PHP8.5 的新 URI 扩展
开发语言·后端·php
少妇的美梦1 小时前
Maven Profile 教程
后端·maven
白衣鸽子1 小时前
RPO 与 RTO:分布式系统容灾的双子星
后端·架构
Jagger_1 小时前
SOLID原则与设计模式关系详解
后端
间彧1 小时前
Java: HashMap底层源码实现详解
后端