文章目录
- 1 问题背景
- 2 前言
- 3 ThreadLocal简介及用法
- 4 ThreadLocal的作用
- 5 回顾ThreadLocal的使用
- 6 Thread | ThreadLocalMap | ThreadLocal | Entry | Key | Value之间的类关系
- 7 ThreadLocal在JVM中的分布
- 8 ThreadLocal是怎么实例化的?
- 9 ThreadLocalMap是怎么初始化的?
- 10 为什么Thread类有ThreadLocalMap类型的变量,但ThreadLocalMap的定义是在ThreadLocal中?
- 11 为什么不用Thread对象作为key,而用ThreadLocal?
- 12 ThreadLocalMap怎么解决hash冲突?
- 13 ThreadLocalMap中的key是弱引用,Java中都有什么引用?
- 14 为什么ThreadLocalMap的key要设计成弱引用类型?
- 15 如果我要塞很多变量到ThreadLocalMap中,是不是要声明多个ThreadLocal对象?
1 问题背景
从大三暑期实习到毕业转正,不知不觉已经工作1年了,感觉自己什么都不会。2022年4月13日凌晨,闲逛牛客的1-3年社招的面经,发现很基本都是偏向校招风格的问底层实现原理,很少问到项目或者业务相关的东西。阅读至少3篇面经,基本上都是问
volatile
关键字、Java锁知识(CAS、锁升级、Java锁与synchronize
的区别)、synchronize
关键字、ThreadLocal
、线程、JVM、分布式锁。其中ThreadLocal
、Java锁知识、分布式锁出现的频率极高。 由于笔者在公司的项目中也用到ThreadLocal
,就先研究它。
参考自:
- JDK 1.8 源码
- ThreadLocal简介及用法
- 一个ThreadLocal和面试官大战300回合
2 前言
笔者已毕业工作10个月了,加上实习有1年半的工作经验了。工作中用到的东西基本都处于“只懂得用,却不知其原理”的状态,这样肯定不行的。本篇博客会尽量从0基础的角度去介绍ThreadLocal的底层实现原理,其中会穿插源码,如果看不懂某一处,不必纠结,反复阅读多几次,联系上下文才会有效果。笔者是第二次研究ThreadLocal了。第一次仅从概念上了解它。
本博文如果阐述有错误,请评论区指正。如有不理解可评论区留言。
3 ThreadLocal简介及用法
如果有小伙伴还没用过ThreadLocal或者没了解过ThreadLocal基本组成,可阅读以下笔者写的简介博文:
4 ThreadLocal的作用
ThreadLocal类提供线程局部变量。防止多线程并发处理请求的时候发生串数据。到底有什么作用?下图解释:
如上图所示,若干个请求发送到后端服务器,服务器分配线程去处理请求。一个线程处理一个请求。A线程处理张三的请求,B线程处理李四的请求。为了防止并发情况下A线程访问到了李四的数据,因此将张三的数据跟A线程绑定,线程处理完请求后解除绑定。
5 回顾ThreadLocal的使用
本文将以ThreadLocal的使用为切入点展开研究,以下仅为一个拦截器的伪代码,伪代码降低理解的难度,不必纠结Java语法,伪代码如下所示:
public class MyInterceptor implements Interceptor{
/**
* new一个ThreadLocal对象,而这个Map<String, Object>就是线程局部变量
*/
private static final ThreadLocal<Map<String, Object>> INFO= ThreadLocal.withInitial(HashMap::new);
public boolean preHandle() {
INFO.get().put("user_id", 1001);
Integer userId = (Integer) INFO.get().get("user_id");
return true;
}
public void postHandle() {
}
public void afterCompletion() {
INFO.remove();
}
}
代码中的Map<String, Object>
就是线程局部变量
6 Thread | ThreadLocalMap | ThreadLocal | Entry | Key | Value之间的类关系
下面给出JDK源码中他们之间的类关系,为便于理解,笔者会省略非必要的代码。:
// 线程类
public class Thread {
// 一个线程对象持有一个ThreadLocalMap的引用,而ThreadLocalMap是被定义在ThreadLocal里面的
ThreadLocal.ThreadLocalMap threadLocals = null;
}
public class ThreadLocal {
static class ThreadLocalMap {
// ThreadLocalMap底层实现是Entry数组,原理和HashMap类似,底层也是数组实现。但是他们解决hash冲突的方法不一样,后面会详细讲解
private Entry[] table;
// key用了弱引用包装,ThreadLocal对象作为key
static class Entry extends WeakReference<ThreadLocal<?>> {
// value就是线程局部变量,也就是ThreadLocal对象存的变量
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
7 ThreadLocal在JVM中的分布
为方便理解文章后面部分的知识,在此先给出ThreadLocal在JVM中的分布。如下图所示:
此处结合第5小节的用例代码与上面的jvm分布图来讲解:
- 假设现在有一个请求发到后端服务器。
- 后端服务器针对每一个接收到的请求,都会分配一个线程去处理(有兴趣可以了解
Linux IO
模型、NIO
知识点)。此时被分配出来处理请求的线程,就跟Java对象的Thread对象关联起来,在JVM中都会通过Thread对象操作线程,一个Thread引用也会指向该Thread对象,如上图所示。 - 而通过本文第6节的类关系可以看出,Thread对象持有ThreadLocalMap的引用,目前仍为null,即指向null对象。
- 处理请求的过程来到本文第5小节的拦截器。首先使用
ThreadLocal.withInitial(HashMap::new);
实例化了一个ThreadLocal对象,并用INFO
引用指向该实例。ThreadLocal对象保存了一个Map
类型的变量,这个Map类型的变量就是线程局部变量
。如上面分布图所示,此时就有INFO
引用指向堆中的ThreadLocal对象。 - 实例化ThreadLocal对象的细节,此处简单介绍,本文后面会详细源码讲解。将ThreadLocal对象作为
key
、Map类型的变量作为value
存入ThreadLocalMap
,即Entry类型的数组,存到数组的哪个位置上呢?底层是计算ThreadLocal对象的hashcode
,用hashcode
对Entry数组的长度作%
模运算,得出值作为数组下标。如上面JVM分布图所示,此时Thread对象指向的ThreadLocalMap已经不为空了,ThreadLocalMap中有一个Entry,它的key指向ThreadLocal对象(并且该指向是被弱引用包装了的),它的value指向Map类型的变量。 - 进入拦截器的
preHandle()
方法。使用INFO.get()
从ThreadLocal对象中拿到线程局部变量
(即Map类型的变量),使用INFO.get().put("user_id", 1001);
向Map类型的变量中存入值。使用INFO.get().get("user_id");
从Map类型的变量获取值。 - 进入拦截器的
afterCompletion()
方法。ThreadLocal对象用完后,使用INFO.remove()
清除ThreadLocal对象的强引用,并清除ThreadLocalMap对应的位置上的内容。 - 线程处理请求完毕。
- 负责处理该请求的线程有可能没被销毁(比如被放入线程池中,等待被分配),因此,从上面分布图可知,这条
Thread引用->Thread对象->ThreadLocalMap->Entry->key->ThreadLocal对象
引用链会一直存在,如果key是 强引用 ThreadLocal对象,那么在GC的时候将不会收回该ThreadLocal对象占用的内存,会造成内存泄漏。因此key引用ThreadLcoal对象使用 WeakReference弱引用 类型包装,尽最大努力减少内存泄漏。
8 ThreadLocal是怎么实例化的?
第5小节中使用如下方法实例化:
private static final ThreadLocal<Map<String, Object>> INFO= ThreadLocal.withInitial(HashMap::new);
ThreadLocal.withInitial()
方法如下:
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
supplier其实就是线程局部变量的实例化方法,上面的HashMap::new
就是实例化一个HashMap类型的变量。
而
SuppliedThreadLocal
类如下:
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
该类继承了ThreadLocal类并重写了initialValue()方法
,文章后面会再次出现这个方法,返回值是返回线程局部变量,即ThreadLocal保存的变量,即Supplier实例化出来的变量。
9 ThreadLocalMap是怎么初始化的?
从上面实例化过程的源码可以看出,并没有实例化ThreadLocalMap,那么它是什么时候实例化的?
ThreadLocalMap
是在第一次ThreadLocal.get()
的时候就被实例化了。
阅读下面的源码讲解之前,先看看以下这些问题,带着问题去看源码讲解会更有目标性。问题如下:
- ThreadLocalMap是怎么初始化的?
- Thread对象持有的ThreadLocalMap引用是何时不为NULL的?
- 线程局部变量是何时存到ThreadLocalMap里面的?
- Entry数组初始容量是多少?
- 数组的下标是怎么计算的?
- 计算下标用到的哈希code是怎么得出来的?
- ThreadLocalMap的阈值是怎么计算的?
- 阈值是用来干嘛的?
接下来研究
get()
的源码,请仔细看代码注释,笔者将通过代码注释来讲解,以下源码都是在ThreadLocal
类里面:
public T get() {
// 拿到当前线程
Thread t = Thread.currentThread();
// 拿ThreadLocalMap,由于是第一次调用ThreadLocal.get(),因此拿到的ThreadLocalMap是null
ThreadLocalMap map = getMap(t);
// map为null,不进入if逻辑
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 设置初始化值,就是在里面初始化ThreadLocalMap
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
// 因为第一次执行ThreadLocal.get(),ThreadLocalMap还没有被初始化,因此Thread对象持有的ThreadLocalMap引用还是指向null,因此返回null出去
return t.threadLocals;
}
private T setInitialValue() {
// 该initialValue方法在本文第8小节就提到过,初始化ThreadLocal对象的时候重写了该方法,返回出去的是线程局部变量,因此返回出来的是HashMap对象
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 再拿一次ThreadLocalMap,因为还没有初始化,依旧会拿到null
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化ThreadLocalMap,第一个入参是Thread对象,第二个入参是线程局部变量
createMap(t, value);
// 把线程局部变量返回出去
return value;
}
void createMap(Thread t, T firstValue) {
// 因为createMap()方法在ThreadLocal类里,this就是ThreadLocal对象
// 初始化TreadLocalMap对象,key是ThreadLocal对象,value是线程局部变量
// Thread对象持有的ThreadLocalMap引用指向ThreadLocalMap对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// INITIAL_CAPACITY = 16,创建一个长度为16的Entry类型的数组
table = new Entry[INITIAL_CAPACITY];
// ‘&’运算其实就是用ThreadLocal对象的hashCode与数组的长度做‘%’运算,得出的值作为数组下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 实例化一个Entry对象,key是ThreadLocal对象,value是线程局部变量,放到Entry数组中
table[i] = new Entry(firstKey, firstValue);
// 设置Entry数组存在的元素个数,目前只有1个
size = 1;
// 设置阈值,扩容的时候需要用到该阈值
setThreshold(INITIAL_CAPACITY);
}
// 计算ThreadLocal的hashcode的方法
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
// nextHashCode是一个原子整形,HASH_INCREMENT = 0x61c88647,该值是将隐式顺序线程本地id转换为接近最优分布的乘法哈希值,用于两幂表。说白了就是将hashcode尽量分散,均匀落在两幂表上,减少hash冲突
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 设置阈值
private void setThreshold(int len) {
// 阈值为数组长度的三分之二,Entry数组中元素超过该阈值则进行扩容,扩容为旧数组长度的2倍
threshold = len * 2 / 3;
}
10 为什么Thread类有ThreadLocalMap类型的变量,但ThreadLocalMap的定义是在ThreadLocal中?
ThreadLocalMap的官方解释如下:
ThreadLocalMap is a customized hash map suitable only for maintaining thread local values. No operations are exported outside of the ThreadLocal class. The class is package private to allow declaration of fields in class Thread. To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.
总结就是ThreadLocalMap是用来维护线程局部变量的,就只做这个事情。这也是为什么ThreadLocalMap是Thread的成员变量,但却是ThreadLocal的内部类。因为内部类是非public的,只有包访问权限,同一个包下的类才能访问。Thread与ThreadLocal都是在同一个java.lang
包下的。这样能让使用者知道ThreadLocalMap只做维护线程局部变量这一件事。
11 为什么不用Thread对象作为key,而用ThreadLocal?
既然是线程局部变量,那为什么不用Thread线程对象而用ThreadLocal对象呢?
如果用Thread对象作为key,会有问题。比如我现在已经有一个Map类型的线程局部变量,但此时我又要新增一个线程局部变量存储登录用户的信息。如果ThreadLocalMap的key用Thread线程对象,key都是同一个线程,但是现在对应Map类型的线程局部变量以及存储登录用户的线程局部变量,一个key对应两个value是不合理的。肯定会有一个线程局部变量被覆盖。就像HashMap
往同一个key
先后put 了两个value
。
要新增存储登录用户的线程局部变量要怎么做?
使用ThreadLocal<UserInfo> userInfo = ThreadLocal.withInitial(UserInfo::new);
再新建一个ThreadLocal对象就好了。那么此时ThreadLocalMap就有两个key,一个key是TreadLocal<Map<String, Object>>
对象,另一个key是ThreadLocal<UserInfo>
对象。
新建出来的ThreadLocal对象在被放入ThreadLocalMap时会不会出现位置冲突?即位置已经被别的ThreadLocal对象占了。
会有可能。解决该冲突的方法是开放定址法(假如位置被占用,则放入相邻的下一个空位置),与HashMap采用链表+红黑树不同。
12 ThreadLocalMap怎么解决hash冲突?
众所周知Java8的HashMap发生hash冲突,会采用链表+红黑树的方法来解决。当
链表长度 > 8 & HashMap中的键值对个数 >= 64
时会转成红黑树,当红黑树的个数 < 6
会转成链表。而ThreadLocalMap的方法如下:
ThreadLocalMap采用开放定址法解决hash冲突。如果发生冲突,ThreadLocalMap会直接往后找相邻的下一个位置,如果位置为空,则直接存进去,如果不为空则继续往后找,如果找到Entry数组最后一个位置,再往后找就会从数组第0号位找,继续往后找,直到找到为止。把元素放进数组里面后,如果往后相邻的一个位置有key为空的Entry对象(即没清理掉元素)且Entry元素>=阈值
则会扩容。新容量是旧容量的2倍。
首先key的hashcode的计算算法就已经尽量降低hash冲突。它会使用一个原子整形的变量自增某个长度才作为hashcode。源码如下:
// 计算ThreadLocal的hashcode的方法
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
// nextHashCode是一个原子整形,HASH_INCREMENT = 0x61c88647,该值是将隐式顺序线程本地id转换为接近最优分布的乘法哈希值,用于两幂表。说白了就是将hashcode尽量分散,均匀落在两幂表上,减少hash冲突
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
使用开放定址法解决hash冲突。此处会基于前面已有的情况(已存在
ThreadLocal<Map<String, Object>>
并且已经初始化了ThreadLocalMap
并且是同一个线程)来研究。因为是研究hash冲突,在看源码的时候我们就认为它会存在冲突(即通过ThreadLocal<Map<String, Object>>
计算得出的数组下标是和通过ThreadLocal<UserInfo>
计算得出的下标是相同的)。请仔细阅读注释,笔者通过注释讲解原理:
// new一个存放登录用户线程局部变量的ThreadLocal对象
private static final ThreadLocal<UserInfo> userInfo = ThreadLocal.withInitial(UserInfo::new);
// 在业务中获取线程局部变量
userInfo.get();
// ThreadLocal.get()方法源码
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取ThreadLocalMap,因为第一次调用ThreadLocal.get()已经初始化了ThreadLocalMap,因此map对象不为null
ThreadLocalMap map = getMap(t);
// map不为null,进入if逻辑
if (map != null) {
// 根据key=ThreadLocal<UserInfo>对象从ThreadLocalMap取值,传入ThreadLocal对象作为入参。
// 得详细看getEntry()方法才能直到e对象是否为null。
// getEntry()返回出null,因此e对象是null。其实逻辑上可以直接知道根据ThreadLocal<UserInfo>对象从map中拿entry是拿到null的,因为ThreadLocal对象还没存入ThreadLocalMap
ThreadLocalMap.Entry e = map.getEntry(this);
// e = null, 不进入if
if (e != null) {
T result = (T)e.value;
return result;
}
}
// 执行设置初始值,setInitialValue()源码请从下面找
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
// 计算数组下标,‘&’运算其实就是ThreadLocal<UserInfo>对象的hashcode对数组长度做‘%’运算
int i = key.threadLocalHashCode & (table.length - 1);
// 因为是研究hash冲突,此处我们假定i是与ThreadLocal<Map<String, Object>>得出的i是相同的,即假定他们两计算出来的下标是一样
Entry e = table[i];
// 因为此处e的key是ThreadLocal<Map<String, Object>>, 而入参key是ThreadLocal<UserInfo>。
// e != null 并且 e的key != 入参key.因此进入else逻辑
if (e != null && e.get() == key)
return e;
else
// 继续获取Entry
// 返回值得出是null,回到上面的get()方法中调用getEntry()的地方
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 因为e是空的,不进入while
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
// 返回null,回到上面的getEntry()方法
return null;
}
private T setInitialValue() {
// value = UserInfo对象。因为initialValue()在实例化ThreadLocal对象时就被重写了,方法的返回值是ThreadLocal对象保存的线程局部变量,详情可以看本文第8小节。
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// map已经被初始化,map不为null
ThreadLocalMap map = getMap(t);
// map不为null,进入if
if (map != null)
// 详情看set()源码,请往下找set()源码
map.set(this, value);
else
createMap(t, value);
// 将线程局部变量存入ThreadLocalMap后返回出线程局部变量
return value;
}
// 解决hash冲突的关键方法就在set()里面
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
// 计算数组下标
int i = key.threadLocalHashCode & (len-1);
// 假定存在冲突,所以e不是null,所以第一次进入for循环,
// 第一次执行完for,则调用e = tab[i = nextIndex(i, len)]往后取e相邻的一个位置,如果e是数组最后一个位置,则取数组的第0号位置,以此类推。详情可往下找nextIndex(i, len)源码。
// 往后取e相邻的一个位置,假设该位置为空,则e = null,不符合进入for循环的条件,因此不进入for
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 第一次循环:e的key与入参key是不同的,所以不进入if
// 可以看到如果两个key相同则新值覆盖旧值
if (k == key) {
e.value = value;
return;
}
// 第一次循环:e的key不为null,不进入if
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 将ThreadLocal<UserInfo>存入数组
tab[i] = new Entry(key, value);
// 键值对个数加1
int sz = ++size;
// 如果i的往后一个位置是key为null的键值对(即没清理掉元素),并且数组中的元素个数 >= 阈值,则进行扩容,扩容的新容量是旧容量的2倍
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 数组位置冲突时,则往后找相邻的一个位置,如果相邻节点位于数组最后一个位置,则取第0号位置,相当于一个环形数组
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
13 ThreadLocalMap中的key是弱引用,Java中都有什么引用?
Java中的引用,笔者也有看过八股文的回答,但是一直不理解。再次阅读并细细琢磨才发现,关键字是“只”,建议读者关注以下回答中的“只”字:
- 强引用。这是使用最普遍的引用。
User user = new User();
中的user
对象就是强引用。如果一个对象有强引用,GC时,垃圾回收期不会回收它。即使内存空间不足,JVM宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会随意回收具有强引用的对象来解决内存不足的问题。 - 软引用。如果一个对象只有软引用,当内存充足时,垃圾回收器不会回收它;当内存不足时,垃圾回收器会回收这些对象。
- 弱引用。垃圾回收器一旦扫描到只有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于执行垃圾回收的线程的优先级较低,因此不一定很快就发现这些只有弱引用的对象并清除。
- 虚引用。如果一个对象仅持有虚引用,在任何时候这些对象都可能被垃圾回收器回收。
14 为什么ThreadLocalMap的key要设计成弱引用类型?
为了尽最大努力避免内存泄漏。为什么是尽最大努力而不是直接避免内存泄漏?看下图:
从上图看到,堆中的ThreadLocal对象被两个引用指向。一个是ThreadLocal
引用,是一直指向ThreadLocal
对象,这个是强引用,对应代码是ThreadLocal<Map<String, Object>> INFO = ThreadLocal.withInitial(HashMap::new)
;另一个是ThreadLocalMap的key引用,对应上图中的虚线。因为ThreadLocal对象是被一个强引用指向的,所以该对象不会被回收内存。假如图中的ThreadLocal引用一直指向堆中的ThreadLocal对象,那么就会存在内存泄漏了。
为什么ThreadLocal设计者要将ThreadLocalMap的key设计成弱引用呢?
考虑到线程的生命周期往往都很长。比如线程池,里面的线程会一直存活。根据JVM根搜索算法,上图中会一直存在Thread引用->Thread对象->ThreadLocalMap对象->Entry->key->ThreadLocal对象
这条引用链,假如key是强引用类型,key就一直不会被GC回收,key就一直是不为null,不为null的Entry元素就不会被清理(ThreadLocalMap根据key是否为null来判断是否清理Entry),同时ThreadLocal对象将会一直被一个强引用指向,会有内存泄漏。
将key设计为弱引用,当堆中的ThreadLocal对象只被弱引用指向,ThreadLocal对象就能被回收。
那怎么才能使得ThreadLocal对象只被弱引用指向?
使用完ThreadLocal对象后,手动调用ThreadLocal.remove()
。
ThreadLocal.remove()源码如下,请仔细看注释:
public void remove() {
// 获取到当前线程持有的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// m不为null
if (m != null)
// 执行remove(),传入ThreadLocal作为参数
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算下标
int i = key.threadLocalHashCode & (len-1);
// 第一次循环,e不为null,符合条件,进入for执行第一次循环
// 一直往后扫描,直到扫到空元素位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// e的key与入参key是否一致,因为e的位置有可能是被其他元素解决hash冲突放进来的
if (e.get() == key) {
// 执行清除,动作就是将key对象置空
e.clear();
// 清除key为null的元素
expungeStaleEntry(i);
return;
}
}
}
// 该方法被定义在Reference类中
public void clear() {
// this是指key对象,此处将key对象置空
this.referent = null;
}
// 清除key为null的元素
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清除陈旧的条目,将值置空
tab[staleSlot].value = null;
// 将元素置空
tab[staleSlot] = null;
// 键值对个数减1
size--;
// Rehash until we encounter null
Entry e;
int i;
// 从被清除元素的位置开始,往后找,只要元素不为空,就进入for循环,直到遇到元素为kong才结束循环
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 扫描到如果key为空,证明该元素可以被清除了,则将元素的值,元素都置空,键值对个数减1
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 如果key不为空,则重新hash下标并存储
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
// 返回出扫描结束的位置
return i;
}
15 如果我要塞很多变量到ThreadLocalMap中,是不是要声明多个ThreadLocal对象?
考虑到要存多个变量,那可以用Map结构,我们直接创建一个线程局部变量为Map类型的ThreadLocal对象就行了。代码如下:
public static final ThreadLocal<Map<String, Object>> INFO = ThreadLocal.withInitial(HashMap::new);
我们文章一开始就是实例化这种线程局部变量为Map
类型的ThreadLocal
对象。