ThreadLocal 介绍
我们来看一下 Java 官方文档中的描述
ThreadLocal 类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal 实例通常来说都是 private static 类型的,用于关联线程和线程上下文。
从官方文档我们可以得知,ThreadLocal 提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
在线程并发的场景下,我们可以通过 ThreadLocal ,在同一线程,不同组件之间传递公共变量,每个线程的变量都是独立的,不会互相影响
ThreadLocal 的常用方法
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建 ThreadLocal 对象 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
ThreadLocal 与 Synchronized 的区别
虽然 ThreadLocal 模式与 Synchronized 关键字都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。
Synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用时间换空间的方式,只提供了一份变量,让不同的线程排队访问 | 采用空间换时间的概念,为每个线程都提供一份变量副本,从而实现同时访问而互不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
在一些场景中,虽然使用 ThreadLocal 和 Synchronized 都能解决问题,但是使用 ThreadLocal 可以使程序拥有更高的并发性。
ThreadLocal 实现的好处
在一些特定场景下,ThreadLocal 方案有两个突出的优势:
- 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题。
- 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
ThreadLocal 的内部结构
最早期的设计
每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,这样就能达到各个线程的局部变量隔离的效果。
现在的设计
在 JDK8 中 ThreadLocal 的设计是这样的:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 才是真正要存储的值 object。
- 每个 Thread 线程内部都有一个 Map(ThreadLocalMap)。
- Map 里面存储 ThreadLocal 对象 key 和线程的变量副本 value。
- Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
思考:这种设计上的变化有什么好处?
- 每个 Map 存储的 Entry 数量变少,因为原来的 Entry 数量是由 Thread 决定,而现在是由 ThreadLocal 决定的。真实开发中,Thread 的数量远远大于 ThreadLocal 的数量。
- 当 Thread 销毁的时候,ThreadLocalMap 也会随之销毁,因为 ThreadLocal 是存放在 Thread 中的,随着 Thread 销毁而消失,能降低开销。
ThreadLocalMap 源码分析
在分析 ThreadLocal 方法的时候,我们了解到 ThreadLocal 的操作实际上是围绕 ThreadLocalMap 展开的。
ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部的 Entry 也是独立实现。
成员变量
// 初始容量 - 必须是 2 的整次幂
private static final int INITIAL_CAPACITY = 16;
// 存放数据的 table,Entry 类的定义在下面分析,同样,数组的长度必须是 2 的整次幂
private Entry[] table;
// 数组里面 entries 的个数,可以用于判断 table 当前使用量是否超过阈值
private int size = 0;
// 进行扩容的阈值,表使用量大于它的时候进行扩容
private int threshold; // Default to 0
跟 HashMap 类似,
INITIAL_CAPACITY
代表这个 Map 的初始容量;table
是一个Entry
类型的数组,用于存储数据;size
代表表中的存储数目;threshold
代表需要扩容时对应的size
的阈值。
存储结构 - Entry
Entry 继承 WeakReference,并且用 ThreadLocal 作为 key。如果 key 为 null(entry.get() == null),意味着 key 不再被引用,因此这时候 entry 也可以从 table 中清除。
static class Entry extends weakReference<ThreadLocal<?>>{
object value;
Entry(ThreadLocal<?>k,object v){
super(k);
value = v;
}
}
在 ThreadLocalMap
中,也是用 Entry
来保存 K-V 结构数据的。不过 Entry
中的 key 只能是 ThreadLocal
对象,这点在构造方法中已经限定死了。
另外,Entry
继承 WeakReference
,也就是key(ThreadLocal)
是弱引用,其目的是将 ThreadLocal
对象的生命周期和线程生命周期解绑。
内存泄漏的原因
在使用 ThreadLocal 的过程中有时候会有内存泄漏的情况发生,这个内存泄漏跟 Entry 中使用了弱引用的 key 有关系吗?我们一起来分析一下
概念回顾
内存泄漏
- Memory overflow:内存溢出,没有足够的内存提供申请者使用。
- Memory leak:内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统溃等严重后果。内存泄漏的堆积终将导致内存溢出。
弱引用
Java 中的引用有 4 种类型:强、软、弱、虚。
当前这个问题主要涉及到强引用和弱引用:
- 强引用:就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
- 弱引用:垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
假设一
假设 ThreadLocalMap 中的 key 使用了强引用,那么会出现内存泄漏吗?
- 假设在业务代码中使用完 ThreadLocal,threadLocal Ref 被回收了。
- 但是因为 ThreadLocalMap 的 Entry 强引用了 ThreadLocal,造成 ThreadLocal 无法被回收。
- 在没有手动删除这个 Entry 以及当前线程依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry 就不会被回收(Entry 中包括了 ThreadLocal 实例和 value),导致 Entry 内存泄漏。
也就是说,ThreadLocalMap 中的 key 使用了强引用,是无法完全避免内存泄漏的。
假设二
假设 ThreadLocalMap 中的 key 使用了弱引用,那么会出现内存泄漏吗?
- 同样假设在业务代码中使用完 ThreadLocal ,threadLocal Ref 被回收了。
- 由于 ThreadLocalMap 只持有 ThreadLocal 的弱引用,没有任何强引用指向 threadlocal 实例,所以 threadlocal 就可以顺利被 GC 回收,此时 Entry 中的 key=null。
- 但是在没有手动删除这个 Entry 以及当前线程依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry-> value,value 不会被回收,而这块 value 永远不会被访问到了,导致 value 内存泄漏。
也就是说,ThreadLocalMap 中的 key 使用了弱引用,也有可能内存泄漏。
真实原因
比较以上两种情况,我们就会发现,内存泄漏的发生跟 ThreadLocalMap 中的 key 是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?
细心的同学会发现,在以上两种内存泄漏的情况中,都有两个前提:
- 没有手动删除这个 Entry
- 当前线程依然运行
因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏。
如何避免内存泄漏
两种方式:
- 使用完 ThreadLocal,调用其 remove 方法删除对应的 Entry
- 使用完 ThreadLocal,当前 Thread 也随之运行结束
第二种方式稍微复杂一点,由于 ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以它的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前 Thread 也随之执行结束,ThreadLocalMap 自然也会被 GC 回收,从根源上避免了内存泄漏。
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的,而是接着放入了线程池中。
为什么使用弱引用
只要记得在使用完 ThreadLocal 及时的调用 remove,无论 key 是强引用还是弱引用都不会有问题。那么为什么 key 要用弱引用呢?
事实上,在 ThreadLocalMap 中的 set / getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null)进行判断,如果为 null 的话,那么是会对 value 置为 null 的。
这就意味着使用完 ThreadLocal,当前 Thread 依然运行的前提下,就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 中的任一方法的时候会被清除,从而避免内存泄漏。