前言
想必很多朋友对 ThreadLocal 并不陌生,而本人也在项目中应用到了 ThreadLocal,今天我们就来一起探讨下 ThreadLocal 的使用方法和实现原理。首先,本文先谈一下对 ThreadLocal 的理解,然后根据 ThreadLocal 类的源码分析了其实现原理和使用需要注意的地方,最后给出了 ThreadLocal 应用场景。
对 ThreadLocal 的理解
ThreadLocal 为解决多线程的并发问题提供了一种新的思路。ThreadLocal,顾名思义是线程的一个本地化对象,当工作于多线程中的对象使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程分配一个独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量。
ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。
从上面的结构图,我们已经窥见 ThreadLocal 的核心机制:
- 每个 Thread 线程内部都有一个 Map。
- Map 里面存储线程本地对象(key)和线程的变量副本(value)
- 但是,Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 Map 获取和设置线程的变量值。
代码示例:
1 | class Scratch { |
虽然程序里面是操作的同一个变量 localVariable,但是不同线程都有自己的一份拷贝。
深入解析 ThreadLocal 类
先了解一下 ThreadLocal 类提供的几个方法:
1 | // get() 方法用于获取当前线程的副本变量值 |
public T get()
1 | public T get() { |
public void set(T value)
1 | public void set(T value) { |
public void remove()
1 | public void remove() { |
Hash 冲突怎么解决?
和 HashMap 的最大的不同在于,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测(开放寻址法)的方式,所谓线性探测,就是根据初始 Key 的 Hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经有其他 Key 值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap 解决 Hash 冲突的方式就是简单的步长加 1 或减 1,寻找下一个相邻的位置。
1 | /** |
显然 ThreadLocalMap 采用线性探测的方式解决 Hash 冲突的效率很低,如果有大量不同的 ThreadLocal 对象放入 Map 中时发生冲突,或者发生二次冲突,则效率很低。
所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到 Map 中的 Key 都是相同的 ThreadLocal,如果一个线程要保存多个变量,就需要创建多个 ThreadLocal,多个 ThreadLocal 放入 Map 中时会极大的增加 Hash 冲突的可能。
如何避免泄漏
ThreadLocalMap 原理
ThreadLocal 的原理:每个 Thread 内部维护着一个 ThreadLocalMap(初始容量 16,负载因子 2/3,解决冲突的方法是再 hash 法),它是一个 Map。这个映射表的 Key 是一个弱引用,其实就是 ThreadLocal 本身,Value 是真正存的线程变量 Object。也就是说 ThreadLocal 本身并不真正存储线程的变量值,它只是一个工具,用来维护 Thread 内部的 Map,帮助存和取。注意上图的虚线,它代表一个弱引用类型,而弱引用的生命周期只能存活到下次 GC 前。
ThreadLocal 为什么会内存泄漏?
ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被 Entry 中的 Key 引用的,因此如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。这个时候就会出现 Entry 中 Key 已经被回收,出现一个 null Key 的情况,外部读取 ThreadLocalMap 中的元素是无法通过 null Key 来找到 Value 的。因此如果当前线程的生命周期很长,一直存在,那么其内部的 ThreadLocalMap 对象也一直生存下来,这些 null Key 就存在一条强引用链的关系一直存在:Thread–>ThreadLocalMap–>Entry–>Value,这条强引用链会导致 Entry 不会回收,Value 也不会回收,但 Entry 中的 Key 却已经被回收的情况,造成内存泄漏。
但是 JVM 团队已经考虑到这样的情况,并做了一些措施来保证 ThreadLocal 尽量不会内存泄漏:在 ThreadLocal 的 get()、set()、remove() 方法调用的时候会清除掉线程 ThreadLocalMap 中所有 Entry 中 Key 为 null 的 Value,并将整个 Entry 设置为 null,利于下次内存回收。
但这样也并不能保证 ThreadLocal 不会发生内存泄漏,例如:使用 static 的 ThreadLocal,延长了 ThreadLocal 的生命周期,可能导致的内存泄漏。分配使用了 ThreadLocal 又不再调用 get()、set()、remove() 方法,那么就会导致内存泄漏。
ThreadLocal 如何避免泄漏?
既然 Key 是弱引用,那么我们要做的事,就是在调用 ThreadLocal 的 get()、set() 方法时完成后再调用 remove() 方法,将 Entry 节点和 Map 的引用关系移除,这样整个 Entry 对象在 GCRoots 分析后就变成不可达了,下次 GC 的时候就可以被回收。如果使用 ThreadLocal 的 set() 方法之后,没有显示的调用 remove() 方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完 ThreadLocal 之后,记得调用 remove() 方法。
在使用线程池的情况下,没有及时清理 ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。
应用场景
ThreadLocal 适用于如下两种场景:
每个线程需要有自己单独的实例
实例需要在多个方法中共享,但不希望被多线程共享
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLocal 可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。
ThreaLocal 的 JDK 文档中说明:ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread。如果我们希望通过某个类将状态(例如用户 ID、事务 ID)与线程关联起来,那么通常在这个类中定义 private static 类型的 ThreadLocal 实例。
1 | public class ThreadContext { |
ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。作用:提供一个线程内公共变量(比如本次请求的用户信息),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
还记得 Hibernate 的 session 获取场景吗?
1 | private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>(); |
为什么?每个线程访问数据库都应当是一个独立的 Session 会话,如果多个线程共享同一个 Session 会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢 Session,提高并发下的安全性。使用 ThreadLocal 的典型场景正如上面的数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
总结
- ThreadLocal 并不解决线程间共享数据的问题
- ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
- ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
- ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
- ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
参考博文
[1]. ThreadLocal - 面试必问深度解析
[2]. 谈谈 Java 中的 ThreadLocal
[3]. Java 进阶(七)正确理解 Thread Local 的原理与适用场景
Java 并发编程之美系列
- Java 并发编程之美(一):并发队列 Queue 原理剖析
- Java 并发编程之美(二):线程池 ThreadPoolExecutor 原理探究
- Java 并发编程之美(三):异步执行框架 Eexecutor
- Java 并发编程之美(四):深入剖析 ThreadLocal
- Java 并发编程之美(五):揭开 InheritableThreadLocal 的面纱
- Java 并发编程之美(六):J.U.C 之线程同步辅助工具类
- Java 并发编程之美(七):透彻理解 Java 并发编程
- Java 并发编程之美(八):循序渐进学习 Java 锁机制