什么是ThreadLocal

ThreadLocal类用来提供线程内部的局部变量
同一个ThreadLocal对象在不同线程调用get()时会得到独立的值。通常来说都是private static类型的,用于关联线程和线程上下文

特性:

  • 线程安全:多线程场景下保证同时操作共享数据安全。
  • 数据传递:通过ThreadLocal在同一线程,不同组件中传递数据。
  • 线程隔离:每个线程的ThreadLocal都是独立的,互不影响。

常用方法

  • ThreadLocal() 创建ThreadLocal对象
  • public void set( T value) 设置当前线程绑定的局部变量
  • public T get() 获取当前线程绑定的局部变量
  • public void remove() 移除当前线程绑定的局部变量

ThreadLocal与synchronized的区别
两者都是解决线程并发安全工具,但使用场景不同

原理
ThreadLocal:空间换时间,为每一个线程提供变量副本,线程间互不干扰。
synchronized:时间换空间,通过加锁让线程排队方式访问共享变量,同一时刻只能有一个线程操作。
关注点
ThreadLocal:隔离线程之间的数据
synchronized:保护共享数据的互斥访问
性能开销
ThreadLocal:无锁竞争,性能较高(但需注意内存)
synchronized:可能产生锁竞争、上下文切换,性能相对较低。
缺点
ThreadLocal:内存泄漏
synchronized:死锁,线程堵塞
结果表现不同
ThreadLocal:每个线程只对自己的变量修改,线程间不存在协作管理,最终结果独立,不会叠加。
synchronized:所有线程操作同一个共享变量,最终结果是所有线程修改后的综合结果。
实现原理不同
ThreadLocal:每个 Thread 对象内部有一个 ThreadLocalMap,ThreadLocal 实例作为 key,用户数据作为 value 存储在线程自己的 map 中。
synchronized:基于对象头中的 monitor 锁,通过 monitorenter/monitorexit 指令或 JVM 层面的锁优化(偏向锁、轻量级锁、重量级锁)实现互斥。
资源释放不同
ThreadLocal:需要手动调用 remove() 清理,否则在线程池场景可能造成内存泄漏。
synchronized:锁自动释放(异常或代码块结束时)。

ThreadLocal的优势

  • 传递数据 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题
  • 线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失

应用场景

  • Web 请求中传递当前用户信息,避免层层传参,且不同请求天然隔离。
  • 数据库连接、事务上下文,同一线程内共享,不同线程隔离。
  • 每个线程需要独立的 SimpleDateFormat 实例

ThreadLocal的内部结构

每个Thread维护一个ThreadLocalMap,key是ThreadLocal实例本身,value才是真正要存储的值Object。

具体过程
1:每个线程都有一个ThreadLocalMap
2:map里面存储的 key是ThreadLocal对象,value是线程变量副本
3:map由ThreadLocal维护,通过get()、set()、获取或操作变量值
4:不同线程获取副本时,数据隔离,互不干扰。

ThreadLocalMapThreadLocal内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
INITIAL_CAPACITY代表这个Map的初始容量;table 是一个Entry 类型的数组,用于存储数据;size 代表表中的存储数目; threshold 代表需要扩容时对应 size 的阈值。

在ThreadLocalMap中,用Entry来保存K-V结构数据。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑

弱引用(WeakReference):当弱引用所指向的对象没有被任何强引用(除弱引用自身外)引用时,GC 回收,并清除该弱引用

内存泄漏问题

当外部(局部变量/实例变量)不再持有 ThreadLocal 对象的强引用时,由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向ThreadLocal实例, 所以ThreadLocal就可以顺利被gc回收,此时Entry中的key=null。

但由于线程存活依旧持有ThreadLocalMap (Thread 对象的成员变量),ThreadLocalMap持有 Entry[] table 数组并强引用每一个 Entry 对象。所以没有手动删除这个Entry以及线程依然运行的前提下,也存在有强引用链
CurrentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问,导致value内存泄漏。

【外部强引用链】(用户代码持有)
用户代码
   └─ 局部变量/实例字段/静态字段(强引用) → ThreadLocal 对象

【内部存储结构】(线程持有)
Thread 对象 (GC Root)
   └─ threadLocals 字段(强引用) → ThreadLocalMap 对象
        └─ table 数组(强引用) → Entry[] 数组
             └─ Entry 对象(强引用)
                  ├─ key(弱引用) → ThreadLocal 对象 (与上述同一个)
                  └─ value(强引用) → 用户对象 (如 10MB 数组)

Entry 继承自 WeakReference,其中的 key 实际是弱引用所指的对象,通过 entry.get() 获取。

对象通过 set() 存入当前线程的 ThreadLocalMap 后,Entry 的 value 字段就会强引用它们。

threadLocalMap 的 Entry 强引用了threadLocal。

如果key使用强引用:
当外部(局部变量/实例变量)不再持有 ThreadLocal 对象的强引用时,threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。Entry 对象本身仍然被 ThreadLocalMap 强引用(Entry[] 数组持有),并且 value 强引用指向用户对象(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。

内存泄漏的真实原因

  1. 没有手动删除这个Entry: 只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏
  2. CurrentThread依然运行: ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。

ThreadLocal内存泄漏的根源是: 由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。

为什么使用弱引用

使用线程池时,线程结束是不会销毁的。也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。

在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。线程依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:
弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏

hash冲突的解决

ThreadLocalMap使用线性探测法来解决哈希冲突。

当插入键值对时,先通过hash计算出一个索引 。如果当前索引已经被占用而且不是当前要查找的 key,则依次检查下一个位置,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

查找和删除也遵循同样的探测序列。

优点
数据量小: ThreadLocalMap 通常只存储少量键值对(一个线程可能只有几个 ThreadLocal 变量)。线性探测在小规模数据下性能好,且无需额外的链表节点对象,节省内存。
缓存友好: 线性探测内存是连续的,对 CPU 缓存更友好。
实现简单: 开放地址法实现简单,无需处理链表结构。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐