ThreadLocal 介绍
ThreadLocal 介绍
ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
总结:
- 线程并发:在多线程并发的场景下
- 传递数据:我们可以通过 ThreadLocal 在同一线程,不同组件中传递公共变量
- 线程隔离:每个线程的变量都是独立的,不会互相影响
常用方法
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建 ThreadLocal 对象 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
使用案例
1 |
|
从结果可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。下面我们来看下采用 ThreadLocal 的方式来解决这个问题的例子。
public class MyDemo1 {
1 |
|
ThreadLocal 类与 synchronized 关键字
1 |
|
ThreadLocal 与 synchronized 的区别
1 |
|
虽然 ThreadLocal 模式与 synchronized 关键字都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。
总结:
在刚刚的案例中,虽然使用 ThreadLocal 和 synchronized 都能解决问题,但是使用 ThreadLocal 更为合适,因为这样可以使程序拥有更高的并发性。
ThreadLocal、Thread、ThreadLocalMap 三者的关系
每一个 Thread 都对应一个 ThreadLocalMap,该 map 在线程创建时已经创建出来,生命周期与 Thread 相同。ThreadLocalMap 中存放 Entry 结点(<k,v> 结构),每一个 entry 结点对应一个 ThreaLocal。而 ThreadLocal 是 Thread 储存在 ThreadLocalMap 中的 value 的 key,获取那一个结点。每一个 ThreadLocal 能够储存一个 value,调用多次 set 方法会发送覆盖。
Hash 冲突问题
因为根据 ThreadLocal 寻找 entry 结点这一过程涉及到 hash 算法,当得到的 hash 值发生冲突时(概率小),会采用线性探测寻找下一个位置的方法解决,效率很低。因此,建议每个线程只存一个变量(只有一个 ThreadLocal)就不存在 Hash 冲突的问题。
ThreadLocal 的内部结构
通过以上的学习,我们对 ThreadLocal 的作用有了一定的认识。现在我们一起来看一下 ThreadLocal 的内部结构,探究它能够实现线程数据隔离的原理。
常见误解
如果我们不去看源代码的话,可能会猜测 ThreadLocal 是这样子设计的:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK 最早期的 ThreadLocal 确实是这样设计的,但现在早已不是了。
现在的设计
但是,JDK 后面优化了设计方案,在 JDK8 中 ThreadLocal 的设计是:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 才是真正要存储的值 object。具体的过程是这样的:
- 每个 Thread 线程内部都有一个 Map(ThreadLocalMap)
- Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value)
- Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
对比
从上面变成 JDK8 的设计有什么好处?
- 每个 Map 存储的 Entry 数量变少,因为原来的 Entry 数量是由 Thread 决定,而现在是由 ThreadLocal 决定的
- 真实开发中,Thread 的数量远远大于 ThreadLocal 的数量
- 当 Thread 销毁的时候,ThreadLocalMap 也会随之销毁,因为 ThreadLocal 是存放在 Thread 中的,随着 Thread 销毁而消失,能降低开销。
ThreadLocal 核心方法源码
基于 ThreadLocal 的内部结构,我们继续分析它的核心方法源码,更深入的了解其操作原理。
除了构造方法之外,ThreadLocal 对外暴露的方法有以下 4 个
方法声明 | 描述 |
---|---|
protected T initialValue() | 返回当前线程局部变量的初始值 |
public void set(T value) | 返回当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
set 方法
代码流程
- 首先获取当前线程,并根据当前线程获取一个 Map
- 如果获取的 Map 不为空,则将参数设置到 Map 中(当前 ThreadLocal 的引用作为 key)
- 如果 Map 为空,则给该线程创建 Map,并设置初始化值
get 方法
代码执行流程
- 首先获取当前线程,根据当前线程获取一个 Map
- 如果获取的 Map 不为空,则在 Map 中以 ThreadLocal 的引用作为 key 来在 Map 中获取对应的 Entrye,否则转到第四步
- 如果 e 不为 null,则返回 e.value,否则转到第四步
- Map 为空或者 e 为空,则通过 initialValue 函数获取初始值 value,然后用 ThreadLocal 的引用和 value 作为 firstKey 和 firstValue 创建一个新的 Map
总结:先获取当前线程的 ThreadLocal 变量,如果存在则返回值,不存在则创建并返回初始值
remove 方法
代码执行流程
- 首先获取当前线程,并根据当前线程获取一个 Map
- 如果获取的 Map 不为空,则移除当前 ThreadLocal 对象对应的 Entry
initialValue 方法
此方法的作用是返回该线程局部变量的初始值。
- 这个方法是一个延迟调用方法,从上面的代码我们得知,在 set 方法还未调用而先调用了 get 方法时才执行,并且仅执行 1 次。
- 这个方法缺省实现直接返回一个 null。
- 如果想要一个除 null 之外的初始值,可以重写此方法。(备注:该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的)
ThreadLocalMap 源码分析
在分析 ThreadLocal 方法的时候,我们了解到 ThreadLocal 的操作实际上是围绕 ThreadLocalMap 展开的。
ThreadLocalMap 的源码相对比较复杂,我们从以下三个方面进行讨论。
基本结构
ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部的 Entry 也是独立实现。
成员变量
1 |
|
跟 HashMap 类似,INITIAL_CAPACITY 代表这个 Map 的初始容量;table 是一个 Entry 类型的数组,用于存储数据;size 代表表中的存储数目;threshold 代表需要扩容时对应的 size 的阈值。
存储结构 - Entry
1 |
|
在 ThreadLocalMap 中,也是用 Entry 来保存 K-V 结构数据的。不过 Entry 中的 key 只能是 ThreadLocal 对象,这点在构造方法中已经限定死了。
另外,Entry 继承 WeakReference,也就是 key(ThreadLocal)是弱引用,其目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑。
弱引用和内存泄漏
有些程序员在使用 ThreadLocal 的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟 Entry 中使用了弱引用的 key 有关系。这个理解其实是不对的。
我们先来回顾这个问题中涉及的几个名词概念,再来分析问题。
内存泄漏相关概念
Memory overflow:内存溢出,没有足够的内存提供申请者使用。
Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统溃等严重后果。I 内存泄漏的堆积终将导致内存溢出。
弱引用相关概念
Java 中的引用有 4 种类型:强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:
强引用(”Strong”Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还 “活着”,垃圾回收器就不会回收这种对象。
弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
如果 key 使用强引用,那么会出现内存泄漏?
假设 ThreadLocalMap 中的 key 使用了强引用,那么会出现内存泄漏吗?
此时 ThreadLocal 的内存图(实线表示强引用)如下:
- 假设在业务代码中使用完 ThreadLocal,threadLocal Ref 被回收了
- 但是因为 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收。
- 在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry 就不会被回收(Entry 中包括了 ThreadLocal 实例和 value),导致 Entry 内存泄漏。
也就是说,ThreadLocalMap 中的 key 使用了强引用,是无法完全避免内存泄漏的。
如果 key 使用弱引用,那么会出现内存泄漏?
- 同样假设在业务代码中使用完 ThreadLocal,threadLocal Ref 被回收了。
- 由于 ThreadLocalMap 只持有 ThreadLocal 的弱引用,没有任何强引用指向 threadlocal 实例,所以 threadloca 就可以顺利被 gc 回收,此时 Entry 中的 key=null。
- 但是在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry-> value,value 不会被回收,而这块 value 永远不会被访问到了,导致 value 内存泄漏。
也就是说,ThreadLocalMap 中的 key 使用了弱引用,也有可能内存泄漏。
出现内存泄漏的真实原因
比较以上两种情况,我们就会发现,内存泄漏的发生跟 ThreadLocalMap 中的 key 是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?
细心的同学会发现,在以上两种内存泄漏的情况中,都有两个前提:
- 没有手动删除这个 Entry
- CurrentThread 依然运行
第一点很好理解,只要在使用完 ThreadLocal,调用其 remove 方法删除对应的 Entry,就能避免内存泄漏。
第二点稍微复杂一点,由于 ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以它的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前 Thread 也随之执行结束,ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。
综上,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread - 样长,如果没有手动删除对应 key 就会导致内存泄漏。
为什么要使用弱引用?
根据刚才的分析,我们知道了:无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
- 使用完 ThreadLocal,调用其 remove 方法删除对应的 Entry
- 使用完 ThreadLocal,当前 Thread 也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的,而是接着放入了线程池中。
也就是说,只要记得在使用完 ThreadLocal 及时的调用 remove,无论 key 是强引用还是弱引用都不会有问题。
那么为什么 key 要用弱引用呢?
事实上,在 ThreadLocalMap 中的 set /getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null)进行判断,如果为 null 的话,那么是会对 value 置为 nul 的。
这就意味着使用完 ThreadLocal,CurrentThread 依然运行的前提下,就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收,对应的 value 在下一次 ThreadLocalMap 调用 set,get,remove 中的任一方法的时候会被清除,从而避免内存泄漏。
Hash 冲突的解决
hash 冲突的解决是 Map 中的一个重要内容。我们以 hash 冲突的解决为线索,来研究一下 ThreadLocalMap 的核心源码。
首先从 ThreadLocal 的 set 方法入手
1 |
|
这个方法我们刚才分析过,其作用是设置当前线程绑定的局部变量
- 首先获取当前线程,并根据当前线程获取一个 Map
- 如果获取的 Map 不为空,则将参数设置到 Map 中(当前 ThreadLocal 的引用作为 key)(这里调用了 ThreadLocalMap 的 set 方法)
- 如果 Map 为空,则给该线程创建 Map,并设置初始值(这里调用了 ThreadLocalMap 的构造方法)
这段代码有两个地方分别涉及到 ThreadLocalMap 的两个方法,我们接着分析这两个方法
构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
构造函数首先创建一个长度为 16 的 Entry 数组,然后计算出 firstKey 对应的索引,然后存储到 table 中,并设置 size 和 threshold。
重点分析:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
关于:threadLocalHashCode
这里定义了一个 Atomiclnteger 类型,每次获取当前值并加上 HASHINCREMENT,HASH_INCREMENT =
0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在 2 的 n 次方的数组里,也就是 EntryI table 中,这样做可以尽量避免 hash 冲突。
关于 &(INITIAL_CAPACITY-1)
计算 hash 的时候里面采用了 hashCode&(size-1)的算法,这相当于取模运算 hashCode% size 的一个更高效的实现。正是因为这种算法,我们要求 size 必须是 2 的整次幂,这也能保证保证在索引不越界的前提下,使得 hash 发生冲突的次数减小。
Get 方法
代码执行流程
- 首先还是根据 key 计算出索引 i,然后查找位置上的 Entry,
- 若是 Entry 已经存在并且 key 等于传入的 key,那么这时候直接给这个 Entry 赋新的 value 值,
- 若是 Entry 存在,但是 key 为 null,则调用 replaceStaleEntry 来更换这个 key 为空的 Entry,
- 不断循环检测,直到遇到为 null 的地方,这时候要是还没在循环过程中 return,那么就在这个 null 的位置新建一个 Entry,并且插入,同时 size 增加 1。
最后调用 cleanSomeSlots,清理 key 为 null 的 Entry,最后返回是否清理了 Entry,接下来再判断 sz 是否 >=
thresgold 达到了 rehash 的条件,达到的话就会调用 rehash 函数执行一次全表的扫描清理。
线性探测法解决 Hash 冲突
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
举个例子,假设当前 table 长度为 16,也就是说如果计算出来 key 的 hash 值为 14,如果 table [14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 1401 得到 15,取 table [15] 进行判断,这个时候如果还是冲突会回到 0,取 table [0],以此类推,直到可以插入。
按照上面的描述,可以把 Entry table 看成一个环形数组。
ThreadLocal 使用场景
源码使用场景
ThreadLocal 的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。
例如,用于 Spring 实现事务隔离级别的源码
Spring 采用 Threadlocal 的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理 connection 对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring 框架里面就是用的 ThreadLocal 来实现这种隔离,主要是在 TransactionSynchronizationManager
这个类里面,代码如下所示:
1 |
|
Spring 的事务主要是 ThreadLocal 和 AOP 去做实现的,我这里提一下,大家知道每个线程自己的链接是靠 ThreadLocal 保存的就好了
用户使用场景 1
除了源码里面使用到 ThreadLocal 的场景,你自己有使用他的场景么?
之前我们上线后发现部分用户的日期居然不对了,排查下来是 SimpleDataFormat 的锅,当时我们使用 SimpleDataFormat 的 parse () 方法,内部有一个 Calendar 对象,调用 SimpleDataFormat 的 parse () 方法会先调用 Calendar.clear(),然后调用 Calendar.add (),如果一个线程先调用了 add () 然后另一个线程又调用了 clear (),这时候 parse () 方法解析的时间就不对了。
其实要解决这个问题很简单,让每个线程都 new 一个自己的 SimpleDataFormat 就好了,但是 1000 个线程难道 new1000 个 SimpleDataFormat?
所以当时我们使用了线程池加上 ThreadLocal 包装 SimpleDataFormat,再调用 initialValue 让每个线程有一个 SimpleDataFormat 的副本,从而解决了线程安全的问题,也提高了性能。
用户使用场景 2
我在项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
使用到类似责任链模式,给每个方法增加一个 context 参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了 ThreadLocal 去做了一下改造,这样只需要在调用前在 ThreadLocal 中设置参数,其他地方 get 一下就好了。
1 |
|
我看了一下很多场景的 cookie,session 等数据隔离都是通过 ThreadLocal 去做实现的
在 Android 中,Looper 类就是利用了 ThreadLocal 的特性,保证每个线程只存在一个 Looper 对象。
1 | JAVA |