一、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,优化内存
缺点:高并发大量写入时,线性寻址变长,读写性能下降明显