ThreadLocal
ThreadLocal 概述
什么是 ThreadLocal,用于解决什么问题?用在什么场景合适?
先来看看官方文档的描述:
[!quote]
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)
该类提供了 线程局部变量,这些变量和普通变量不一样,当每个线程访问自己的这个变量的时候,访问到的都是独属于自己的变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
按我自己的话说:
[!note]
ThreadLocal 是一个将在多线程中为每一个线程创建单独的变量副本的类,每个线程在访问它的时候,访问到的都是自己的那一份副本,把他设置成 private static 的话,在当前线程内就都能访问到,可以用来存储和当前线程相关的一些状态,并且避免多线程因为操作共享变量导致的数据不一致
解决线程安全的思路:
- 互斥同步:Synchronized 和 ReentrantLock
- 非阻塞同步:CAS,原子变量
- 无同步方案:ThreadLocal ,可重入代码
因此,ThreadLocal 很适合用于保证线程安全,或者存储一些和线程绑定的数据。
快速开始
提到 ThreadLocal 被提到应用最多的是 session 管理和数据库链接管理,这里以数据库连接访问为例
1 | |
这样每个线程调用 ConnectionManager 的 getConnection 的时候,拿到的都是属于自己的连接,如果不用 ThreadLocal 的话,那么就可能导致同一个连接被多个线程使用,造成各种问题。
不过一般用连接池管理,而不是 ThreadLocal
ThreadLocal 原理
ThreadLocalMap
在 Thread 类中,有这么一个字段 threadLocals
用来存储所有 ThreadLocal 变量,以 Map 的形式,Key 是 ThreadLocal 变量的 HashCode,Value 就是 ThreadLocal 存储的内容。
但这个 Map 结构有些不一样:
- 它没有实现 Map 接口;
- 它没有 public 的方法, 最多有一个 default 的构造方法, 因为这个 ThreadLocalMap 的方法仅仅在 ThreadLocal 类中调用, 属于静态内部类
ThreadLocalMap的 Entry 实现继承了WeakReference<ThreadLocal<?>>- 该方法仅仅用了一个 Entry 数组来存储 Key, Value; Entry 并不是链表形式, 而是每个 bucket 里面仅仅放一个 Entry;
ThreadLocal.set
该方法的步骤:
- 使用 Key 的 HashCode 对 Entry 数据取余,相当于一个哈希的过程,获取存储的位置
- 然后从哈希出的位置开始 线性探测,如果找到了那么设置为当前最新的值返回
- 在线程探测的过程中,如果发现有的 key 是 null,那么说明这个 ThreadLocal 被清理了,则将该 Entry 设置为当前插入的 Value返回
- 直到遇见了空槽也没找到匹配的ThreadLocal对象,那么在此空槽处安排ThreadLocal对象和缓存的value。然后将哈希表扩容,如果没有元素被清理,那么就要检查当前元素数量是否超过了容量阙值(数组大小的三分之二),以便决定是否扩容
ThreadLocal.get
其实很简单,就是去哈希表找,找不到则用线性探测法继续往后找
get 原理
当调用 ThreadLocal 变量的 get() 方法的时候:
会首先获取当前的线程,然后以当前线程为参数,去获取 ThreadLocal 的值
具体步骤:
- 通过当前线程拿到 Thread 类中的 map
- 如果 map 不为空,则以 ThreadLocal 变量本身为 Key 获取存储的 Value,并返回
- 如果 map 为空,调用
setInitialValue方法返回
setInitialValue 方法如下:
- 首先调用 initialValue 方法, 产生一个 Value 对象
- 继续查看当前线程的 threadLocals 是不是空的, 如果 ThreadLocalMap 已被初始化, 那么直接将产生的对象添加到 ThreadLocalMap 中, 如果没有初始化, 则创建并添加对象到其中;
注意:创建 ThreadLocal 的时候是可以重载
initialValue方法的
set 原理
当调用 ThreadLocal 的 set(T value) 方法去设置值的时候,也会先去获取当前线程,然后以当前线程为参数来设置 ThreadLocal 的值
具体步骤:
- 根据当前线程获取 map
- 如果 map 不为空,则直接以 ThreadLocal 变量为 Key,set 的值为 Value 插入 map
- 如果 mpa 为空,则创建 map 同时插入 Key,Value
ThreadLocal 内存泄漏
为什么 ThreadLocal 可能会导致内存泄漏呢?看以下场景
1 | |
在这种场景下,当程序运行完成后,由于 ThreadLocal 是局部变量,运行完后,不再持有 ThreadLocal 的强引用,而在 ThreadLocalMap 中的 Entry 里,ThreadLocal 作为 Key 是弱引用,没有其他强引用指向,所以会被 GC 回收掉。但是 Value 被 Entry 强引用,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
但 ThreadLocalMap 的 get 和 set 方法会清除线程ThreadLocalMap里所有key为null的value,可以作为一层预防措施。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用线程池的时候,这个线程执行任务结束,
ThreadLocal对象被回收了,线程放回线程池中不销毁,这个线程一直不被使用,导致内存泄漏。 - 使用线程池的时候,线程执行任务结束,但是
ThreadLocal是private static的,导致强引用一直存在,虽然这些数据已经不用了,但是还在内存中 - 分配使用了
ThreadLocal又不再调用get(),set(),remove()方法,那么这个期间就会发生内存泄漏。
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
==怎么防止 ThreadLocal 内存泄漏呢? 使用完后及时 remove==
ThreadLocal 使用场景
- Web中的 Session 管理
- 微服务架构中,跨服务调用时的跟踪ID(如分布式链路追踪中的traceId)可以通过
ThreadLocal在线程内传递,确保所有日志记录都能包含这一标识,便于后续的日志分析和问题排查。 - 当每个线程需要独立拥有一个对象实例(如工具类或资源)以避免状态冲突时,使用
ThreadLocal可以为每个线程提供独立的实例副本