写点什么

彻底搞懂 ThreadLocal

用户头像
千珏
关注
发布于: 2021 年 04 月 22 日

作用

多线程环境下,对于同一个变量,每个线程保存自己线程私有的值

明确:ThreadLocal 不是一个 map,而是一个 key,真正的 map 是在 Thread 类持有的 ThreadLocalMap,这个 map 放在了 ThreadLocal 类里面去实现

Thread、ThreadLocal、ThreadLocalMap 的关联如下


常见用法

//常用于全局变量的维护public class ContextVariable {  private static ThreadLocal<String> userId = new ThreadLocal<>();
public static void setUserId(String uid){ userId.set(uid); }
public static String getUserId(){ return userId.get(); }}
//假设该拦截器放在HttpServeltRequest拦截器链上面,那么每个线程调用时都会记录该线程解析出来的userIdpublic class JwtInterceptor { public void parseJwt(String token){ ContextVariable.setUserId(extractUserId(token)); } private String extractUserId(String token){ //do something with token return "uid"; }}
//最终,业务上直接使用该userIdpublic class Service { public void doService(){ String userId = ContextVariable.getUserId(); //do something with userId }
}
复制代码


源码分析

ThreadLocal 类一共暴露出了四个实例方法

  • ThreadLocal:构造方法,负责生成实例的 hashCode

  • get:获取此实例中保存的值

  • set:设置此实例中保存的值

  • remove:移除此实例中保存的值

set

首先说下 set 的实现原理,大致的流程图如下:


以上最重要的细节没有体现,包括 expungeStaleEntry 和 rehash

由于 ThreadLocalMap 使用线性探测法来解决哈希冲突,因此每次往 map 里面新增数据或者更新已有数据但该数据不在首次哈希索引上时,都会触发一次清理,清理掉 key 为 null 的 entry,并必要时对清理路径上的非 null 值做重新 hash,目的是为了保证下次 get 时可以最大可能在首次哈希索引上就能找到,而不需要探测。

试想一下,如果没有清理和 rehash 的操作,则哈希表就会退化成一个线性查找的数组。

下面用一些实例来说明清理过程:


注意,以上并不是 threadLocal 本身为 null,而是 Entry 持有的 threadLocal 弱饮用被 gc 回收了,或者应用程序在 threadLocal 上面调用了 remove

由于 threadLocalMap 是每个线程私有维护的,会随着线程的释放而被 GC 回收,因此线程的数量跟 threadLocalMap 的容量无关,而是 threadLocal 变量的多少决定了 threadLocalMap 的大小,决定了是否清理和扩容

rehash 过程如下:

  • 先执行一遍完整的清理,把 key 引用为 null 的 Entry 回收掉,不为 null 的重新 hash 放回正确的位置

  • 如果上述清理完成后数组的实际保存的元素大小超过 3/4 的 threshold(即数组大小的一半),则扩容

  • 生成一个 2 倍大小的数组,依次把原数组中的 entry hash 到新数组位置

get

理解了上述 set 的原理之后 get 其实非常简单了

  • 如果线程的 threadLocalMap 还没有初始化,则初始化一个 map,并以 value=null 生成一个 entry 作为初始值

  • 根据 threadLocal 的 hash 值找到数组下标,如果 entry 为 null,则和上面一样以 value=null 生成一个 entry 作为初始值并返回

  • 如果 entry 不为 null,判断 entry 里面的 threadLocal 是否和 key 一样,如果一样则返回 value,否则依次往后找

  • 往后找的过程中如果发现 entry 的 key 为 null 了,则也会主动触发一次清理过程


remove

和 get 同样的方式首先定位 entry,把 entry 的 threadLocal 设置为 null,并触发一次清理

对比 ThreadLocalMap 和 HashMap

相同点

  • 底层都是采用数组实现

  • 都有负载因子和扩容的过程

不同点

  • HashMap 类似于一个全局容器,根据 key 获取 value;ThreadLocalMap 更像是一个私有变量,获取过程不需要 key,因为 key 就是 threadLocal 变量

  • HashMap 提供的方法很多,ThreadLocalMap 则只有 3 个方法可以用

  • 哈希冲突的解决方式不一样,HashMap 使用链表和红黑树解决冲突,ThreadLocalMap 使用线性探测法解决哈希冲突

  • Entry 不一样,HashMap 的 Entry 里面保存了 key 和 value,而 ThreadLocalMap 的 Entry 可以认为是一个只保存了 value 的 threadLocal 的弱引用

  • ThreadLocalMap 在 get 和 set 过程发生哈希冲突时,会清理掉保存 null 引用的 entry,而 HashMap 不会清理

发布于: 2021 年 04 月 22 日阅读数: 27
用户头像

千珏

关注

还未添加个人签名 2017.12.20 加入

还未添加个人简介

评论

发布
暂无评论
彻底搞懂ThreadLocal