Java并发编程:ThreadLocal
一、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 ThreadLocalString 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 ThreadLocalString 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全方位对比对比维度synchronizedThreadLocal并发原理时间换空间同一资源排队访问串行执行空间换时间线程独有副本并行无竞争执行作用目的多线程共享同一变量保证修改安全线程变量隔离线程不共享变量锁机制内置排他锁存在线程阻塞、上下文切换无锁设计零阻塞、无排队开销资源开销内存开销低线程共用一份变量内存开销高每个线程独立存储副本适用场景多线程共享修改同一变量变量线程独享无需跨线程共享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回收效率更高弱引用优化keyThreadLocal对象使用弱引用方便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数据时自动触发返回默认初始值存入当前线程ThreadLocalMap3.1.3 使用方式// lambda重写初始化方法创建即赋值 private static final ThreadLocalInteger 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获取线程内部绑定的ThreadLocalMapMap不为空以当前ThreadLocal的引用为key覆盖value值Map为空新建ThreadLocalMap初始化存入Entry3.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 继承 WeakReferenceThreadLocal?说明key(ThreadLocal实例)是弱引用 # 2. 构造方法把ThreadLocal传给父类WeakReferencevalue为业务存储的数据强引用 # 3. 当外部不存在ThreadLocal强引用时GC会回收ThreadLocal对象entry.get()返回null # 4. ThreadLocalMap扩容/清理时会遍历table删除key为null的Entry防止内存泄漏 # 5. 隐患value是强引用若线程长期存活线程池不手动remove会导致value无法回收造成内存泄漏 static class Entry extends WeakReferenceThreadLocal? { /** 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弱引用可自动GCvalue强引用永远不会自动断开引用。4.3.3 ThreadLocal内存泄漏真实原因标准答案ThreadLocal对象无外部强引用触发GCEntry.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. nextHashCodeAtomicInteger原子整数多线程并发创建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_INCREMENTHASH_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寻址寻址遇到keynull脏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优化内存缺点高并发大量写入时线性寻址变长读写性能下降明显