一、ThreadLocal基础全解
1.1 ThreadLocal定义
ThreadLocal是java.lang包下线程本地存储工具类 ,核心作用:实现线程数据隔离,为每一个线程创建专属独立变量副本,线程之间变量互不共享、互不干扰,从底层规避多线程并发竞争问题。还可以通过ThreadLocal在同一线程,不同组件中传递公共变量。
核心底层定论:ThreadLocal本身不存储数据,仅作为存取入口,数据真正存储在绑定当前线程的ThreadLocalMap中。
核心注解:解决多线程共享变量并发修改、线程安全问题,属于空间换时间并发方案。
1.2 ThreadLocal标准使用规范
1.2.1 标准使用步骤
-
定义static final修饰ThreadLocal常量(生产强制规范)
-
set():绑定当前线程专属变量值
-
get():获取当前线程专属变量副本
-
remove():线程业务结束手动清除数据,规避内存泄漏
1.2.2 基础使用语法
// 生产写法:static final 全局唯一,避免重复创建
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
// 设置当前线程专属值
THREAD_LOCAL.set("用户登录信息");
// 获取当前线程专属值
String value = THREAD_LOCAL.get();
// 移除当前线程数据,必写收尾代码
THREAD_LOCAL.remove();
1.3 线程隔离实战案例
案例效果:多线程共用同一个ThreadLocal对象,各自存取数据互不干扰,读取不到其他线程数据。
先写一个普通的线程存取内容的代码:
# 代码说明:未使用ThreadLocal,成员变量content属于共享对象,多线程并发读写会出现数据错乱、线程数据互相覆盖
public class ThreadLocalDemo {
private String content;
private String getContent() {
return content;
}
private void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}
});
thread.setName("线程" + i);
thread.start();
}
}
}
输出结果:
线程0--->线程1的数据
线程2--->线程2的数据
线程1--->线程1的数据
线程4--->线程4的数据
线程3--->线程3的数据
从结果可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。下面我们来看下采用ThreadLocal 的方式来解决这个问题的例子。
# ThreadLocal:为每个线程单独存储一份私有变量,线程间数据隔离,无并发冲突
public class ThreadLocalDemo {
# 创建ThreadLocal,每个线程单独持有一份String副本
private static ThreadLocal<String> contentLocal = new ThreadLocal<>();
private String getContent() {
return contentLocal.get();
}
private void setContent(String content) {
contentLocal.set(content);
}
public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
# 使用完移除,避免内存泄漏
contentLocal.remove();
}
});
thread.setName("线程" + i);
thread.start();
}
}
}
输出:
线程0--->线程0的数据
线程4--->线程4的数据
线程1--->线程1的数据
线程3--->线程3的数据
线程2--->线程2的数据
案例结论:多线程读写互不影响,无并发竞争,无需加锁即可保证线程安全。
1.4 ThreadLocal与synchronized全方位对比
|------|---------------------|----------------------|
| 对比维度 | synchronized | ThreadLocal |
| 并发原理 | 时间换空间,同一资源排队访问,串行执行 | 空间换时间,线程独有副本,并行无竞争执行 |
| 作用目的 | 多线程共享同一变量,保证修改安全 | 线程变量隔离,线程不共享变量 |
| 锁机制 | 内置排他锁,存在线程阻塞、上下文切换 | 无锁设计,零阻塞、无排队开销 |
| 资源开销 | 内存开销低,线程共用一份变量 | 内存开销高,每个线程独立存储副本 |
| 适用场景 | 多线程共享修改同一变量 | 变量线程独享,无需跨线程共享 |
1.5 ThreadLocal核心优势
-
无锁并发:彻底规避synchronized锁竞争、线程阻塞、上下文切换开销,并发性能极高
-
线程数据强隔离:线程变量完全独立,天然线程安全,无需手动管控并发
-
上下文透传:一站式实现线程内全局参数传递,省去方法多层传参冗余代码
-
使用极简:API轻量化,set/get/remove即可完成数据管控,上手成本低
-
降低代码耦合:统一封装线程专属上下文,解耦业务参数传递逻辑
1.6 ThreadLocal原生缺点
-
内存开销大:每条线程独立存储变量副本,线程量大时占用堆内存陡增
-
无法跨线程共享:数据仅限当前线程使用,线程间无法通信取值
-
存在内存泄漏风险:配合线程池复用线程时,不手动remove极易内存泄漏
-
强依赖线程生命周期:线程销毁前,绑定数据会常驻内存
-
类隔离限制:父子线程无法默认继承数据,需要InheritableThreadLocal拓展
1.7 线上生产高频使用场景
-
登录用户上下文透传:拦截器存入当前登录用户信息,全局业务任意位置获取,无需接口传参
-
数据库连接隔离:单线程绑定独立数据库Connection,保证事务同线程复用连接
-
MDC日志链路追踪:存储traceId,同一线程全链路日志绑定同一个追踪ID,排查线上问题
-
时间格式化工具隔离:SimpleDateFormat非线程安全,ThreadLocal绑定线程独享工具实例
-
脱敏、国际化线程缓存:缓存当前线程用户语种、脱敏规则,全局复用
二、ThreadLocal内部结构、设计原理
通过以上的学习,我们对ThreadLocal的作用有了一定的认识。现在我们一起来看一下ThreadLocal的内部结构,探究它能够实现线程数据隔离的原理。
2.1 JDK8 ThreadLocal全新设计原理
2.1.1 整体层级结构
Thread ----持有--> ThreadLocalMap ----存储--> Entry(key,value)
结构详解:
-
每个Thread线程内部,独有一个ThreadLocalMap成员变量
-
ThreadLocalMap内部存储Entry数组,Entry键值对存储数据
-
Entry.key = ThreadLocal对象(弱引用),Entry.value = 线程绑定业务数据
-
ThreadLocal仅为工具操作入口,不存储任何业务数据
2.1.2 JDK7与JDK8结构区别
JDK7:所有ThreadLocal共用一个全局Map,存储多线程数据,极易内存溢出
JDK8优化:线程绑定专属Map,数据分散至各自线程,降低内存耦合
2.2 JDK8结构设计优点
-
数据分散存储:Map归属线程自身,线程销毁直接回收Map,回收效率更高
-
弱引用优化key:ThreadLocal对象使用弱引用,方便GC自动回收无用ThreadLocal
-
哈希冲突概率降低:单线程Entry数量少,数组寻址更快,读写效率提升
-
适配线程池:线程复用场景下,仅需清理当前线程Entry,管控粒度更细
2.3 JDK8结构设计缺点
-
key弱引用、value强引用不对称,产生key回收、value滞留的内存泄漏漏洞
-
ThreadLocalMap自定义哈希算法,不使用HashMap链地址法,冲突处理逻辑复杂
-
Entry过期清理为被动触发,不主动读写则无法清理脏数据
-
线程池核心线程常驻不销毁,常驻线程Entry脏数据永久滞留堆内存
三、ThreadLocal核心方法、源码+执行流程全解析
基于ThreadLocal的内部结构,我们继续分析它的核心方法源码,更深入的了解其操作原理。除了构造方法之外,ThreadLocal对外暴露的方法有以下4个:
核心四大方法:get()、set()、remove()、initialValue(),附JDK8完整源码+链路流程
3.1 initialValue() 初始化方法
3.1.1 核心源码
/**
* 返回当前线程对应的ThreadLocal的初始值
*
* 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
* 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
* 通常情况下,每个线程最多调用一次这个方法。
*
* <p>这个方法仅仅简单的返回null {@code null};
* 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
* 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
* 通常, 可以通过匿名内部类的方式实现
*
* @return 当前ThreadLocal的初始值
*/
# initialValue() 源码方法说明:
# 1. 保护方法,默认返回null,线程第一次get()且未执行set()时自动触发执行;
# 2. 若线程提前执行set()存入数据,后续get不会调用该初始化方法;
# 3. 单一线程生命周期内,该方法最多只会执行1次;
# 4. 想要自定义初始值,需要重写该方法,常用匿名内部类/lambda withInitial实现;
# 5. 作用:避免get()直接返回null,给每个线程提供默认初始数据
protected T initialValue() {
return null;
}
3.1.2 执行流程
-
线程首次调用get(),无绑定Entry数据时自动触发
-
返回默认初始值,存入当前线程ThreadLocalMap
3.1.3 使用方式
// lambda重写初始化方法,创建即赋值
private static final ThreadLocal<Integer> LOCAL = ThreadLocal.withInitial(() -> 0);
此方法的作用是返回该线程局部变量的初始值。
- 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
- 这个方法缺省实现直接返回一个null。
- 如果想要一个除null之外的初始值,可以重写此方法。(备注:该方法是一个protected的方法,显然是为了让子类覆盖而设计的)
3.2 set() 设置线程数据方法
3.2.1 核心源码精简版
public void set(T value) {
// 1.获取当前执行线程
Thread t = Thread.currentThread();
// 2.获取线程专属ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 3.Map存在:新增/覆盖Entry键值对
map.set(this, value);
} else {
// 4.Map不存在:创建ThreadLocalMap,存入首个Entry
createMap(t, value);
}
}
3.2.2 完整执行流程
-
获取当前运行线程Thread
-
获取线程内部绑定的ThreadLocalMap
-
Map不为空:以当前ThreadLocal的引用为key,覆盖value值
-
Map为空:新建ThreadLocalMap,初始化存入Entry
3.3 get() 获取线程数据方法
3.3.1 核心源码精简版
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 1.通过当前ThreadLocal获取Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 2.Entry存在,直接返回value
return (T)e.value;
}
}
// 3.Map为空/Entry为空:执行初始化方法
return setInitialValue();
}
3.3.2 执行流程
-
获取当前线程、线程绑定Map
-
匹配当前ThreadLocal对应的Entry
-
匹配成功:返回value
-
匹配失败:则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
总结: 先获取当前线程的ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。
3.4 remove() 移除线程数据方法【生产必调用】
3.4.1 核心源码精简版
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// 删除当前ThreadLocal对应的Entry键值对
m.remove(this);
}
}
3.4.2 执行流程&核心作用
获取当前线程Map,精准删除当前ThreadLocal关联Entry,断开key、value引用,帮助GC回收,唯一主动规避内存泄漏的API。
四、ThreadLocalMap底层源码深度分析
在分析ThreadLocal方法的时候,我们了解到ThreadLocal的操作实际上是围绕ThreadLocalMap展开的。ThreadLocalMap的源码相对比较复杂, 我们从以下几三个方面进行讨论。
4.1 ThreadLocalMap基本结构
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。

4.1.1 核心成员变量
// 底层存储Entry数组,和HashMap数组结构一致
private Entry[] table;
// 数组已存储元素个数
private int size = 0;
// 扩容阈值,默认容量16,负载因子2/3,阈值=10
private int threshold;
// 初始容量,必须为2的幂次方,方便哈希取模寻址
private static final int INITIAL_CAPACITY = 16;
跟HashMap类似,INITIAL_CAPACITY代表这个Map的初始容量;table 是一个Entry 类型的数组,用于存储数据;size 代表表中的存储数目;threshold 代表需要扩容时对应size 的阈值。
4.1.2 存储结构Entry
/*
* Entry继承WeakReference,并且用ThreadLocal作为key.
* 如果key为null(entry.get() == null),意味着key不再被引用,
* 因此这时候entry也可以从table中清除。
*/
# ThreadLocalMap内部静态Entry源码注释解读:
# 1. Entry 继承 WeakReference<ThreadLocal<?>>,说明key(ThreadLocal实例)是弱引用;
# 2. 构造方法把ThreadLocal传给父类WeakReference,value为业务存储的数据(强引用);
# 3. 当外部不存在ThreadLocal强引用时,GC会回收ThreadLocal对象,entry.get()返回null;
# 4. ThreadLocalMap扩容/清理时会遍历table,删除key为null的Entry,防止内存泄漏;
# 5. 隐患:value是强引用,若线程长期存活(线程池),不手动remove会导致value无法回收,造成内存泄漏
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal . */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
核心结论:Entry = 弱引用key(ThreadLocal) + 强引用value(业务数据)
4.2 Java四大引用类型完整拓展
|---------------------|----------------------|---------------|-------------|------------------------|
| 引用类型 | 回收规则定义 | 优点 | 缺点 | 适用场景 |
| 强引用 | 默认引用,GC永不回收,直至引用断开 | 对象常驻、访问速度快 | 极易引发内存泄漏 | 常规业务对象、全局常量 |
| 软引用SoftReference | 内存充足不回收,内存OOM前强制回收 | 缓存可控,兼顾性能与内存 | 回收时机不可控 | 图片缓存、大对象缓存 |
| 弱引用WeakReference | 下次GC到来,无论内存是否充足,直接回收 | 自动释放内存,防泄漏能力强 | 生命周期短,不可常驻 | ThreadLocal key、临时关联对象 |
| 虚引用PhantomReference | 无法获取对象,仅做GC回收通知 | 监控对象回收状态 | 无法使用取值,功能单一 | 堆外内存回收监控 |
4.3 ThreadLocal内存泄漏闭环解析
4.3.1 内存泄漏概念
内存泄漏:程序已不再使用某对象,但是GC无法回收该对象占用堆内存,内存持续堆积,最终导致服务OOM宕机。
4.3.2 内存泄漏与四大引用关系
内存泄漏本质:强引用滞留无法断开 ;ThreadLocal设计不对称:key弱引用可自动GC,value强引用永远不会自动断开引用。
4.3.3 ThreadLocal内存泄漏真实原因(标准答案)
-
ThreadLocal对象无外部强引用,触发GC,Entry.key弱引用被回收置为null
-
当前线程(线程池核心线程)长期存活,线程持有ThreadLocalMap强引用
-
key为null,但value依旧被Entry强引用绑定,无法被GC回收
-
脏Entry堆积,业务不再使用value,内存永久占用,形成内存泄漏
误区纠正:不是弱引用导致泄漏,是key弱引用、value强引用不对称+线程常驻导致泄漏
4.3.4 四种解决内存泄漏方案(优先级排序)
-
业务finally强制remove()(生产最优):业务执行完毕手动删除Entry,断开value引用
-
定义static final ThreadLocal:全局强引用ThreadLocal,避免key被GC误回收
-
线程池自定义包装线程:线程归还池内前,清空当前线程所有ThreadLocal数据
-
依托get/set被动清理:读写Map时,底层自动清理key为null的脏Entry
五、ThreadLocalMap Hash冲突全解
5.1 Hash冲突产生原理
5.1.1 寻址哈希算法
// 哈希寻址公式:天然取模2的幂数组下标
int i = key.threadLocalHashCode & (table.length - 1);
a. 关于firstKey.threadLocalHashCode:
# ThreadLocal 哈希相关源码注释解析
# 1. threadLocalHashCode:每个ThreadLocal实例唯一哈希值,实例创建时一次性初始化,final不可修改
# 2. nextHashCode():静态原子自增方法,每次生成下一个哈希偏移量
# 3. nextHashCode:AtomicInteger原子整数,多线程并发创建ThreadLocal也能保证自增安全
# 4. HASH_INCREMENT = 0x61c88647 黄金分割数,魔数,用于降低哈希冲突,均匀散列到ThreadLocalMap数组
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用
private static AtomicInteger nextHashCode = new AtomicInteger();
//特殊的hash值
private static final int HASH_INCREMENT = 0x61c88647;
这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT= 0x61c88647 ,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry\[\] table中,这样做可以尽量避免hash冲突。
b. 关于& (INITIAL_CAPACITY - 1)
计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。
5.1.3 和HashMap冲突处理区别
HashMap:链地址法,数组+链表+红黑树;ThreadLocalMap:线性探测法,向后遍历空位存放,无链表结构。
5.2 Hash冲突源码执行流程
-
根据threadLocalHashCode计算下标i,命中数组位置
-
下标位置Entry.key == 当前ThreadLocal:直接覆盖value,流程结束
-
下标位置key不为null、且不匹配:判定Hash冲突,线性向后i++寻址
-
寻址遇到key==null脏Entry:复用当前位置,存入新Entry,顺带清理脏数据
-
寻址遇到空位:新建Entry存入,size++,判断是否触发扩容
5.3 ThreadLocalMap解决Hash冲突方案
5.3.1 核心方案:线性探测法
发生冲突后,按照数组下标依次向后遍历,寻找空闲槽位存储数据,同时顺路清理key为null的过期脏Entry,一举两得。
5.3.2 前置优化:自定义哈希值增量
ThreadLocal自定义哈希魔数HASH_INCREMENT = 0x61c88647,黄金分割哈希增量,最大限度打散哈希值,从源头降低Hash冲突概率。
5.3.3 后置兜底:扩容机制
数组元素size达到阈值2/3容量时,触发数组二倍扩容,重新rehash迁移所有Entry,减少数组拥挤,降低后续冲突概率。
5.3.4 冲突优缺点总结
-
优点:结构简单、无链表开销、冲突时自动清理脏Entry,优化内存
-
缺点:高并发大量写入时,线性寻址变长,读写性能下降明显