ThreadLocal原理
ThreadLocal
基本介绍
ThreadLocal 是 JDK 提供的一个工具类,用于线程内部的数据隔离。每个线程通过它都可以拥有属于自己的变量副本,从而实现线程间的数据不共享。
本质:是一个 Map,结构类似于:
1 | Thread { |
ThreadLocal 实例通常来说都是 private static 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程内部都维护着一个自己的 ThreadLocalMap,这个 Map 存的是当前线程所持有的所有 ThreadLocal 变量。
ThreadLocal 作用:
- 线程并发:应用在多线程并发的场景下
- 传递数据:通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度
- 线程隔离:每个线程的变量都是独立的,不会互相影响
| 场景 | 解释 |
|---|---|
| 线程隔离 | 每个线程数据独立,互不干扰 |
| 数据传递 | 在同一线程中跨方法或组件共享数据,避免参数传递 |
| 事务管理、连接池、工具封装 | JDBC连接、日期解析等 |
对比 synchronized:
| 对比项 | synchronized | ThreadLocal |
|---|---|---|
| 思路 | 以时间换空间,多线程排队使用共享变量 | 以空间换时间,每个线程一份变量副本 |
| 解决问题 | 多线程访问共享资源的同步问题 | 多线程之间变量相互隔离问题 |
| 适用场景 | 并发读写同一个资源 | 每个线程需要独立使用变量 |
基本使用
常用方法
| 方法 | 说明 |
|---|---|
set(T value) |
设置当前线程变量的值 |
get() |
获取当前线程变量的值 |
remove() |
移除当前线程变量,防止内存泄漏 |
initialValue()(可重写) |
设置初始值 |
1 | public class MyDemo { |

应用场景
ThreadLocal 适用于下面两种场景:
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
ThreadLocal 方案有两个突出的优势:
- 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题
- 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失
ThreadLocal 用于数据连接的事务管理:

1 | public class JdbcUtils { |
用 ThreadLocal 使 SimpleDateFormat 从独享变量变成单个线程变量:

1 | public class ThreadLocalDateUtil { |

实现原理
ThreadLocal 的设计目的
ThreadLocal 的核心作用是:为每个线程提供变量副本,避免共享冲突,实现线程隔离。
举个形象的比喻:每个线程像一个学生,ThreadLocal 像是发下去的作业纸,每个人写自己的,不会互相干扰。
底层结构演变
JDK8 以前:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,达到各个线程的局部变量隔离的效果。这种结构会造成 Map 结构过大和内存泄露,因为 Thread 停止后无法通过 key 删除对应的数据


JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 是真正要存储的值
- 每个 Thread 线程内部都有一个 Map (ThreadLocalMap)
- Map 里面存储 ThreadLocal 对象(key)和线程的私有变量(value)
- Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰


JDK8 前后对比:
- 每个 Map 存储的 Entry 数量会变少,因为之前的存储数量由 Thread 的数量决定,现在由 ThreadLocal 的数量决定,在实际编程当中,往往 ThreadLocal 的数量要少于 Thread 的数量
- 当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用,防止内存泄露
总结对比(JDK8 前后)
| 对比项 | JDK8 之前 | JDK8 之后 |
|---|---|---|
| Map 所属 | 每个 ThreadLocal 拥有一个 Map | 每个 Thread 拥有一个 Map |
| Map key | 线程对象(Thread) | ThreadLocal 实例 |
| Map value | 局部变量 | 局部变量 |
| 内存占用 | 较高 | 更节省内存 |
| 是否易泄漏 | 是 | 相对更安全(但仍需手动 remove) |

这部分详见JVM面试篇


成员变量
1.每个线程维护自己的 ThreadLocalMap
1
ThreadLocal.ThreadLocalMap threadLocals = null
每个线程对象中都维护着一个 ThreadLocalMap,用于存放线程自己的 ThreadLocal 值。
- Map 的 key 是 ThreadLocal 对象,value 是线程局部变量值。
2.threadLocalHashCode:哈希值
1 | private final int threadLocalHashCode = nextHashCode() |
- 每一个 ThreadLocal 对象实例都有一个唯一的 hash 值,决定它在 ThreadLocalMap 中的位置。
- 使用了斐波那契散列算法,能让哈希值均匀分布,减少哈希冲突。
1 | private static final int HASH_INCREMENT = 0x61c88647; |
这个常量是黄金比例因子,在 Java 中被广泛用于哈希均衡(如 ThreadLocal、ConcurrentHashMap)。
成员方法
方法都是线程安全的,因为 ThreadLocal 属于一个线程的,ThreadLocal 中的方法,逻辑都是获取当前线程维护的 ThreadLocalMap 对象,然后进行数据的增删改查,没有指定初始值的 threadlcoal 对象默认赋值为 null
- initialValue():返回该线程局部变量的初始值
- 延迟调用的方法,在执行 get 方法时才执行该方法缺省(默认)实现直接返回一个 null如果想要一个初始值,可以重写此方法, 该方法是一个
protected的方法,为了让子类覆盖而设计的
- 延迟调用的方法,在执行 get 方法时才执行该方法缺省(默认)实现直接返回一个 null如果想要一个初始值,可以重写此方法, 该方法是一个
1 | protected T initialValue() { |
- 默认返回 null。
- 可重写,设置线程变量的默认值。
只有在调用
get()且当前线程还没有设置值时才会调用(延迟加载)nextHashCode():计算哈希值,ThreadLocal 的散列方式称之为斐波那契散列,每次获取哈希值都会加上 HASH_INCREMENT,这样做可以尽量避免 hash 冲突,让哈希值能均匀的分布在 2 的 n 次方的数组中
1 | private static int nextHashCode() { |
- set(T value):修改当前线程与当前 threadlocal 对象相关联的线程局部变量
1 | public void set(T value) { |
解读:
- 获取当前线程的 ThreadLocalMap
- 如果已存在,就往 Map 中设置
this(当前 ThreadLocal 对象)为 key 的值 如果还没有初始化 Map,就调用
createMap()创建一个新的并设置值get():获取当前线程与当前 ThreadLocal 对象相关联的线程局部变量
1 | public T get() { |
解读:
- 尝试从当前线程中拿到与当前 ThreadLocal 对象关联的值
如果找不到,调用
initialValue()获取默认值,并设置进去remove():移除当前线程与当前 threadLocal 对象相关联的线程局部变量
1 | public void remove() { |
解读:
- 移除当前线程中与当前 ThreadLocal 绑定的那一项。
- 非常关键!线程池环境中若不及时移除,可能造成内存泄漏或数据污染。
小结


ThreadLocalMap
ThreadLocalMap 的结构核心
ThreadLocalMap 是 ThreadLocal 的静态内部类,它本质上是一个定制版哈希表(数组+线性探测)。
成员属性
主要属性
| 属性名 | 作用说明 |
|---|---|
INITIAL_CAPACITY |
初始容量(固定为 16) |
Entry[] table |
存储数据的表,数组结构 |
int size |
当前存储元素数量 |
int threshold |
扩容阈值(2/3 表容量) |
ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部 Entry 也是独立实现
1 | // 初始化当前 map 内部散列表数组的初始长度 16 |
存储结构 Entry:

- Entry 继承 WeakReference,key 是弱引用,目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑
- Entry 限制只能用 ThreadLocal 作为 key,key 为 null (entry.get() == null) 意味着 key 不再被引用,entry 也可以从 table 中清除
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
构造方法:延迟初始化的,线程第一次存储 threadLocal - value 时才会创建 threadLocalMap 对象

1 | ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { |
成员方法
set():添加数据,ThreadLocalMap 使用
线性探测法来解决哈希冲突
- 该方法会一直探测下一个地址,直到有空的地址后插入,若插入后 Map 数量超过阈值,数组会扩容为原来的 2 倍假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个环形数组线性探测法会出现堆积问题,可以采取平方探测法解决在探测过程中 ThreadLocal 会复用 key 为 null 的脏 Entry 对象,并进行垃圾清理,防止出现内存泄漏
1 | private void set(ThreadLocal<?> key, Object value) { |

1 | // 获取【环形数组】的下一个索引 |

- getEntry():ThreadLocal 的 get 方法以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e
1 | private Entry getEntry(ThreadLocal<?> key) { |

- rehash():触发一次全量清理,如果数组长度大于等于长度的
2/3 * 3/4 = 1/2,则进行 resize
1 | private void rehash() { |
Entry 数组为扩容为原来的 2 倍 ,重新计算 key 的散列值,如果遇到 key 为 null 的情况,会将其 value 也置为 null,帮助 GC
1 | private void resize() { |

remove():删除 Entry
1 | private void remove(ThreadLocal<?> key) { |

重要机制总结
| 机制 | 说明 |
|---|---|
| 线性探测 | 哈希冲突时顺序查找下一个空槽 |
| 过期数据处理 | 使用 replaceStaleEntry + cleanSomeSlots 清理 |
| 弱引用 Entry | ThreadLocal 不在引用时能被回收 |
| 自动扩容 | 达到阈值时触发 rehash & resize |
| 内存泄漏预防 | 明确调用 remove() 或手动清理 stale Entry |
ThreadLocalMap的清理机制
- 探测式清理:沿着开始位置向后探测清理过期数据,沿途中碰到未过期数据则将此数据 rehash 在 table 数组中的定位,重定位后的元素理论上更接近
i = entry.key & (table.length - 1),让数据的排列更紧凑,会优化整个散列表查询性能

1 | // table[staleSlot] 是一个过期数据,以这个位置开始继续向后查找过期数据 |


- 启发式清理:向后循环扫描过期数据,发现过期数据调用探测式清理方法,如果连续几次的循环都没有发现过期数据,就停止扫描

1 | // i 表示启发式清理工作开始位置,一般是空 slot,n 一般传递的是 table.length |
内存泄漏
Java 中的内存泄漏是指:对象本该被回收,却因为仍然存在强引用链而无法释放。
- 如果 key 使用强引用:使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收,无法完全避免内存泄漏

- 如果 key 使用弱引用:使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时 Entry 中的 key = null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行,依然存在强引用链,value 不会被回收,而这块 value 永远不会被访问到,也会导致 value 内存泄漏

- 两个主要原因:
- 没有手动删除这个 Entry
- CurrentThread 依然运行


根本原因:ThreadLocalMap 是 Thread的一个属性,生命周期跟 Thread 一样长,如果没有手动删除对应 Entry 就会导致内存泄漏
解决方法:使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以

ThreadLocal 内部解决方法:在 ThreadLocalMap 中的 set/getEntry 方法中,通过线性探测法对 key 进行判断,如果 key 为 null(ThreadLocal 为 null)会对 Entry 进行垃圾回收。所以使用弱引用比强引用多一层保障,就算不调用 remove,也有机会进行 GC


ThreadLocalMap 的变量传递问题
为什么普通 ThreadLocal 无法跨线程传递?
ThreadLocal 是线程本地存储机制,每个线程维护一份自己的副本:
1 | Thread --> ThreadLocalMap --> Entry(ThreadLocal, value) |
每个线程的 ThreadLocalMap 是隔离的,互不访问。因此,普通 ThreadLocal 无法跨线程传递变量。
如何实现变量传递?使用 InheritableThreadLocal
父子线程:创建子线程的线程是父线程,比如实例中的 main 线程就是父线程
ThreadLocal 中存储的是线程的局部变量,如果想实现线程间局部变量传递可以使用 InheritableThreadLocal 类
1 | public static void main(String[] args) { |
这个类自动完成了 ThreadLocalMap 的复制操作,让子线程拥有与父线程一样的副本。
底层原理解析
InheritableThreadLocal 源码

1 | public class InheritableThreadLocal<T> extends ThreadLocal<T> { |
实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法

1 | private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, |

1 | private ThreadLocalMap(ThreadLocalMap parentMap) { |
| 对比项 | ThreadLocal | InheritableThreadLocal |
|---|---|---|
| 数据隔离性 | 每线程独立 | 每线程独立 |
| 支持传递 | ❌ 不支持跨线程 | ✅ 支持从父线程传递 |
| 使用场景 | 一般线程变量隔离 | 子线程需继承父变量 |
| 能否自定义传递值 | ❌ | ✅ 可重写 childValue() |
| 示例应用 | 用户 ID、请求 ID 等 | Web 请求上下文、日志追踪 |

