ThreadLocal
# ThreadLocal 详解
ThreadLocal 概述
ThreadLocal 类用来提供线程内部的局部变量,不同的线程之间不会相互干扰
这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量
在线程的生命周期内起作用,可以减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度
使用
常用方法
方法名 | 描述 |
---|---|
ThreadLocal() | 创建 ThreadLocal 对象 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public T remove() | 移除当前线程绑定的局部变量,该方法可以帮助 JVM 进行 GC |
protected T initialValue() | 返回当前线程局部变量的初始值 |
案例
场景:让每个线程获取其设置的对应的共享变量值
共享变量访问问题案例
1 | /** |
- 使用关键字 Synchronized 关键字加锁解决方案
1 | /** |
- 使用 ThreadLocal 方式解决
1 | public class SolveDemoQuestionByThreadLocal { |
# ThreadLocalMap 内部结果
JDK8 之前的设计
每个 ThreadLocal 都创建一个 ThreadLocalMap,用线程作为 ThreadLocalMap 的 key,要存储的局部变量作为 ThreadLocalMap 的 value,这样就能达到各个线程的局部变量隔离的效果
JDK8 之后的设计
每个 Thread 维护一个 ThreadLocalMap,这个 ThreadLocalMap 的 key 是 ThreadLocal 实例本身,value 才是真正要存储的值 Object
每个 Thread 线程内部都有一个 ThreadLocalMap
Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value)
Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值
对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰
JDK 对 ThreadLocal 这样改造的好处
减少 ThreadLocalMap 存储的 Entry 数量:因为之前的存储数量由 Thread 的数量决定,现在是由 ThreadLocal 的数量决定。在实际运用当中,往往 ThreadLocal 的数量要少于 Thread 的数量
当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用(但是不能避免内存泄漏问题,解决内存泄漏问题应该在使用完后及时调用 remove () 对 ThreadMap 里的 Entry 对象进行移除,由于 Entry 继承了弱引用类,会在下次 GC 时被 JVM 回收)
# ThreadLocal 相关方法源码解析
# set 方法
- 源码及相关注释
1 | /** |
- 相关流程图
- 执行流程
- 获取当前线程,并根据当前线程获取一个 Map
- 如果获取的 Map 不为空,则将参数设置到 Map 中(当前 ThreadLocal 的引用作为 key)
- 如果 Map 为空,则给该线程创建 Map,并设置初始值
# get () 方法
- 源码及相关注释
1 | /** |
- 流程图
执行流程
获取当前线程,根据当前线程获取一个 Map
如果获取的 Map 不为空,则在 Map 中以 ThreadLocal 的引用作为 key 来在 Map 中获取对应的 Entrye,否则转到 4
如果 e 不为 null,则返回 e.value,否则转到 4
Map 为空或者 e 为空,则通过 initialValue 函数获取初始值 value,然后用 ThreadLocal 的引用和 value 作为 firstKey 和 firstValue 创建一个新的 Map
# remove 方法
- 源码及相关注释
1 | /** |
- 执行流程
- 首先获取当前线程,并根据当前线程获取一个 Map
- 如果获取的 Map 不为空,则移除当前 ThreadLocal 对象对应的 entry
initialValue 方法
此方法的作用是返回该线程局部变量的初始值
这个方法是一个延迟调用方法,从上面的代码我们得知,在 set 方法还未调用而先调用了 get 方法时才执行,并且仅执行 1 次
这个方法缺省实现直接返回一个 null
如果想要一个除 null 之外的初始值,可以重写此方法。(备注: 该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的)
源码及相关注释
1 | /** |
ThreadLocalMap 解析
# 内部结构
ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部的 Entry 也是独立实现的,而 Entry 又是 ThreadLocalMap 的内部类,且集成弱引用 (WeakReference) 类。
# 成员变量
1 | /** |
# 弱引用和内存泄漏
弱引用相关概念
强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还 “活着”,垃圾回收器就不会回收这种对象
弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
# 内存泄漏相关概念
Memory overflow: 内存溢出,没有足够的内存提供申请者使用
Memory leak: 内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出
# 内存泄漏与强弱引用关系
ThreadLocal 内存结构
如果 key 使用强引用,也就是上图中的红色背景框部分
业务代码中使用完 ThreadLocal ,threadLocal Ref 被回收了
因为 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收
在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry 就不会被回收(Entry 中包括了 ThreadLocal 实例和 value),导致 Entry 内存泄漏
如果 key 使用弱引用,也就是上图中的红色背景框部分
业务代码中使用完 ThreadLocal ,threadLocal Ref 被回收了
由于 ThreadLocalMap 只持有 ThreadLocal 的弱引用,没有任何强引用指向 threadlocal 实例,所以 threadlocal 就可以顺利被 gc 回收,此时 Entry 中的 key=null
但是在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value 不会被回收, 而这块 value 永远不会被访问到了,导致 value 内存泄漏
# 出现内存泄漏的真实原因
没有手动删除对应的 Entry 节点信息
ThreadLocal 对象使用完后,对应线程仍然在运行
# 避免内存泄漏的的两种方式
使用完 ThreadLocal,调用其 remove 方法删除对应的 Entry
使用完 ThreadLocal,当前 Thread 也随之运行结束
对于第一种方式很好控制,调用对应 remove () 方法即可,但是对于第二种方式,我们是很难控制的,正因为不好控制,这也是为什么 ThreadLocalMap 里对应的 Entry 对象继承弱引用的原因,因为使用了弱引用,当 ThreadLocal 使用完后,key 的引用就会为 null,而在调用 ThreadLocal 中的 get ()/set () 方法时,当判断 key 为 null 时会将 value 置为 null,这就就会在 jvm 下次 GC 时将对应的 Entry 对象回收,从而避免内存泄漏问题的出现。
# hash 冲突问题及解决方法
首先从 ThreadLocal 的 set () 方法入手
1 | public void set(T value) { |
-
构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
-
/* * firstKey : 本ThreadLocal实例(this) * firstValue : 要保存的线程本地变量 */ ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //初始化table table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY]; //计算索引(重点代码) int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //设置值 table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue); size = 1; //设置阈值 setThreshold(INITIAL_CAPACITY); }
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;1
2
3
4
5
6
构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold
分析:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
关于:firstKey.threadLocalHashCodeprivate void set(ThreadLocal<?> key, Object value) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; //计算索引(重点代码,刚才分析过了) int i = key.threadLocalHashCode & (len-1); /** * 使用线性探测法查找元素(重点代码) */ for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //ThreadLocal 对应的 key 存在,直接覆盖之前的值 if (k == key) { e.value = value; return; } // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了, // 当前数组中的 Entry 是一个陈旧(stale)的元素 if (k == null) { //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏 replaceStaleEntry(key, value, i); return; } } //ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。 tab[i] = new Entry(key, value); int sz = ++size; /** * cleanSomeSlots用于清除那些e.get()==null的元素, * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。 * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行 * rehash(执行一次全表的扫描清理工作) */ if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } /** * 获取环形数组的下一个索引 */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }1
2
3
4
5
6
7
这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突
关于:& (INITIAL_CAPACITY - 1)
计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小
ThreadLocalMap中的set方法# 代码执行流程:
1. 首先还是根据 key 计算出索引 i,然后查找 i 位置上的 Entry
2. 若是 Entry 已经存在并且 key 等于传入的 key,那么这时候直接给这个 Entry 赋新的 value 值
3. 若是 Entry 存在,但是 key 为 null,则调用 replaceStaleEntry 来更换这个 key 为空的 Entry
4. 不断循环检测,直到遇到为 null 的地方,这时候要是还没在循环过程中 return,那么就在这个 null 的位置新建一个 Entry,并且插入,同时 size 增加 1
5. 最后调用 cleanSomeSlots,清理 key 为 null 的 Entry,最后返回是否清理了 Entry,接下来再判断 sz 是否 >= thresgold 达到了 rehash 的条件,达到的话就会调用 rehash 函数执行一次全表的扫描清理
- 分析 : ThreadLocalMap 使用线性探测法来解决哈希冲突的
1. 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出
2. 假设当前 table 长度为 16,也就是说如果计算出来 key 的 hash 值为 14,如果 table [14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table [15] 进行判断,这个时候如果还是冲突会回到 0,取 table [0], 以此类推,直到可以插入3. 可以把 Entry [] table 看成一个环形数组
# 关于我
Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!
非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!