前言快速到底
由于 ThreadLocal 的定义不太好理解,所以先看一个 Demo,根据 Demo 的运行结果再理解 ThreadLocal 的定义,这样理解起来就好多了。
示例代码
package com.thread;
public class ThreadLocalTest
{
private static final ThreadLocal<Integer> tl = new ThreadLocal<Integer>() // 这个写法刚开始没注意
{
@Override
protected Integer initialValue()
{
return 0;
};
};
static class MyThread implements Runnable
{
@Override
public void run()
{
System.out.println("线程" + Thread.currentThread().getName() + "的初始值是:" + tl.get());
for (int i = 0; i < 10; i++)
{
try
{
Thread.sleep(200); // show 方法的 test 线程是循环创建五个线程,所以在这里将五个线程全部到这里开始执行
tl.set(tl.get() + i); // 每次都是取这个线程的 key 为 tl 的这个 map,然后加上一个新的值再赋进去
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
System.out.println("线程" + Thread.currentThread().getName() + "的累加值是:" + tl.get());
}
}
public static void main(String[] args)
{
for (int i = 0; i < 5; i++)
{
new Thread(new MyThread()).start(); // 开启五个线程
}
}
}
注:
运行结果
线程Thread-2的初始值是:0
线程Thread-1的初始值是:0
线程Thread-0的初始值是:0
线程Thread-4的初始值是:0
线程Thread-3的初始值是:0
线程Thread-3的累加值是:45
线程Thread-2的累加值是:45
线程Thread-4的累加值是:45
线程Thread-1的累加值是:45
线程Thread-0的累加值是:45
通过以上的 Demo 案例的运行结果(每个线程的初始值都是 0,每个线程的累加值都是 45) 得出一个结论:每个线程的 ThreadLocal 存储的值是互不干扰的。
摘自于 JDK 源码对于 ThreadLocal 的描述:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
翻译过来大概是这样的:
ThreadLocal 类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过这些变量的(指的是 ThreadLocal 变量)get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。
应用场景
在知乎上看到一个举例说的很不错
举个例子,我出门需要先坐公交再做地铁,这里的坐公交和坐地铁就好比是同一个线程内的两个函数,我就是一个线程,我要完成这两个函数都需要同一个东西:公交卡(北京公交和地铁都使用公交卡),假设这两个函数之间不能传递公交卡这个变量(仅仅是业务需求)。我可以这么做:将公交卡事先交给一个机构,当我需要刷卡的时候再向这个机构要公交卡(当然每次拿的都是同一张公交卡)。这样就能达到只要是我(同一个线程)需要公交卡,何时何地都能向这个机构要的目的。有人要说了:你可以将公交卡设置为全局变量啊,这样不是也能何时何地都能取公交卡吗?但是如果有很多个人(很多个线程)呢?大家可不能都使用同一张公交卡吧(我们假设公交卡是实名认证的),这样不就乱套了嘛。现在明白了吧?这就是ThreadLocal设计的初衷:提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。
说明
ThreadLocalMap 不是共享的,每个线程都有自己的 ThreadLocalMap 实例。要搞清楚这个 map 实际上维护的是 ThreadLocal 实例和 set 的值之间的映射关系。 简单举个例子,代码里有3个静态的 ThreadLocal,分别是 a, b, c。有 2 个线程 X 和 Y 在使用这些 ThreadLocal,那么 X 线程设置 c 的值为 100,就是往 X 的 map 里,维护 ( c, 100 ) 这样一个映射关系,同理 X 设置 a 的值为 200,也是往自己的 map 里维护 ( a, 200 )。Y 线程对 a 和 c 这两个 ThreadLocal 调用 get 的时候,是在 Y 的 map 里找 a 和 c 对应的值,结果自然是 null。所以说,ThreadLocal 的线程安全正是因为不同线程的有自己的 ThreadLocalMap,隔离而非共享。
在上面的 Demo 案例中,我对每一个线程仅仅是赋了一个 ThreadLocal(因为只写了一个 ThreadLocal 成员变量),其实每个线程是可以有多个 ThreadLocal 对象的(多创建几个 ThreadLocal 成员变量就可以了);但是每个线程只有一个 ThreadLocalMap 对象 ( ThreadLocal.ThreadLocalMap threadLocals = null; ), 这个 ThreadLocalmap 里面可以存储多个 ThreadLocal-value 这样的键值对。我 Demo 中只设置一个 ThreadLocal 的原因是:你每次都根据这个 ThreadLocal 结合这个线程就可以取到这个线程对应的 ThreadLocalMap 里面 key 为这个 ThreadLocal 的 value 值。如果你一个 Thread 线程跑了多个 ThreadLocal,那这个线程的 ThreadLocalMap 里面就有多个 ThreadLocal-value 这样的键值对。而我上面只是为了演示每个线程中的 ThreadLocalMap 是互不相干的,所以没必要往每个 Thread 中的ThreadLocalMap 里面存放多个 ThreadLocal 对象。
ThreadLocal 源码分析
ThreadLocal其实比较简单,因为类里就三个 public 方法:set(T value)、get()、remove()。先剖析源码清楚地知道ThreadLocal是干什么用的、再使用、最后总结,讲解ThreadLocal采取这样的思路。
set(T value)
先来看看 set 方法的源码:
ThreadLocal.class
1 /**
2 * Sets the current thread's copy of this thread-local variable
3 * to the specified value. Most subclasses will have no need to
4 * override this method, relying solely on the {@link #initialValue}
5 * method to set the values of thread-locals.
6 *
7 * @param value the value to be stored in the current thread's copy of
8 * this thread-local.
9 */
10 public void set(T value) {
11 Thread t = Thread.currentThread();
12 ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap,每个 Thread 只有一个 ThreadLocalMap
13 if (map != null)
14 map.set(this, value); // 在当前线程的 ThreadLocalMap 里面存一个键值对,键是 ThreadLocal,值就是你要存的值;
15 else 也就是下面说的当前线程的 ThreadLocalMap 维护的其实是 ThreadLocal 和 value 这样的一对关系。Thread 持有的是 ThreadLocalMap,而不是 ThreadLocal
16 createMap(t, value);
17 }
ThreadLocal.class
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
Thread.class
ThreadLocal.ThreadLocalMap threadLocals = null;
既然涉及到 ThreadLocalMap,那我们就先来看看 ThreadLocalMap 的一个构造函数,这个构造函数在 set 和 get 的时候都可能会被 间接调用以初始化线程的 ThreadLocalMap
/**
* 构造一个包含 firstKey 和 firstValue 的 map
* ThreadLocalMap 是惰性构造的,所以只有当至少要往里面放一个元素的时候才会构建它。
*/
ThreadLocalMap(java.lang.ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 table 数组
table = new Entry[INITIAL_CAPACITY];
// 用 firstKey 的 threadLocalHashCode 与初始大小 16 取模得到哈希值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 初始化该节点
table[i] = new Entry(firstKey, firstValue);
// 设置节点表大小为1
size = 1;
// 设定扩容阈值
setThreshold(INITIAL_CAPACITY);
}
ThreadLocal$ThreadLocalMap.class
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table; // 很明显 table 这个变量的声明不是在 ThreadLocal 这个类中,而是在 ThreadLocalMap 这个 static 内部类中;因为对于 ThreadLocal 类而言,
// 只需要操作 ThreadLocalMap 对象就可以了,看看源码都是这样做的,而 TthreadLocalMap 中才会定义真正的 table 数组来存储数
重点看一下上面构造函数中的 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 这一行代码。ThreadLocal 类中有一个被 final 修饰的类型为 int 的 threadLocalHashCode,它在该 ThreadLocal 被构造的时候就会生成,相当于一个ThreadLocal的ID,而它的值来源于
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger(); // 这个就是随着 ThreadLocal 对象创建的时候创建的,严格意义上它才是 ThreadLocal 对象的唯一标识 ID
/*
* 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
*/
private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); // threadLocalHashCode = nextHashCode + 0x61c88647 加上一个数也可以称为 ID
}
这个魔数的选取与斐波那契散列有关,0x61c88647 对应的十进制为 1640531527。斐波那契散列的乘数可以用 (long) ((1L << 31) * (Math.sqrt(5) - 1)) 可以得到 2654435769,如果把这个值给转为带符号的 int,则会得到 -1640531527。换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1)) 得到的结果就是 1640531527。通过理论与实践,当我们用 0x61c88647 作为魔数累加为每个 ThreadLocal 分配各自的 ID 也就是 threadLocalHashCode 再与 2 的幂取模,得到的结果分布很均匀。ThreadLocalMap 使用的是 线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。这就回答了上文抛出的为什么大小要为 2 的幂的问题。为了优化效率。(比如:你第一个存进来的 ThreadLocal 有一个 threadLocalHashCode ,然后与魔数相加,得到一个位置;第二个进来也是同样的算法得到一个位置,这样计算出来的每一个位置分布就比较均匀,然后线性探测就有效果。)
接下来就看 ThreadLocalMap 的 set 方法
ThreadLocal$ThreadLocalMap.class
private void set(ThreadLocal key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1);
// 线性探测 for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
// entry 获取 k 都是通过 get() 方法
ThreadLocal k = e.get(); // 由于 Entry 中的 get() 方法是继承 Reference 的。实际上,Entry 此时有两个成员变量,除了 Object value 还有一个
// private T referent 。但是这个变量是 Reference 类私有的,所以只能通过父类的 public 方法进行访问。这里就是通过继承
// Reference 类的 get() 方法进行获取值得访问的,而赋值的时候是通过 super(k) 调用父类的构造方法进行访问的;
// 由于 Entry (Entry 也是一个弱引用类)继承的 WeakReference 的泛型是 ThreadLocal 的,所以 Entry 引用的对象是
// ThreadLocal 类型的对象。结合上面的叙述,一旦一个 ThreadLocal 对象被一个的引用地址被赋值给了 Entry 的 private T referent
// 成员变量,就代表这个 ThreadLocal 在堆中的对象被 Entry 这个弱引用给引用了。
if (k == key) { // 如果找到了,把新的值赋给它 e.value = value;
return; } if (k == null) { // 如果在线性向后的探测中发现有 key 为 null 的 entry (这个 key 为 null,是由于 ThreadLocal 的对象被回收了) replaceStaleEntry(key, value, i); // 这里是对 table 的环形线性探测当 key 为 null 的情况的详细算法,也是最重要的 return; } } tab[i] = new Entry(key, value); // 如果上面的整个环形探测都是做完了既没有找到这个 key,也没有发现 k ==null 的,就把创建一个新的 entry 放到这个位置 int sz = ++size; // 这个位置就是每一个 ThreadLoca 的 ID int i = key.threadLocalHashCode & (len-1); )的位置,然后 table 中的 entry 的个数 + 1; if (!cleanSomeSlots(i, sz) && sz >= threshold) // 尝试着对 table 进行清理,如果一个 key 都没有被清理出去,并且当前 table 大小已经超过阈值了, rehash(); // 则做一次 rehash,rehash 函数会调用一次全量清理(Expunge all stale entries in the table.)
// 即 expungeStaleEntries,清理完了之后 table 大小
} // 超过了 threshold - threshold / 4,则调用 resize() 方法进行扩容 2 倍
// 在这里我有一个疑问?就是上面的探测结束了 既没有找到这个 key,也没有发现 k ==null 的 情况才会走到这里,那么既然上面
// 都没有发现 k == null,的那这里为什么还要进行清理呢?除非有一种情况,那就是这个向后的线性探测并没有能够把这个 entry
// 环全部都探测完。
ThreadLocal$ThreadLocalMap.class
private Entry[] table;
ThreadLocal$ThreadLocalMap$Entry.class -------> Entry 又是 ThreadLocalMap 中的一个内部类
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
为什么上面的 Entry 会是这样的定义形式,为什么要用弱引用?(关于 Java 的几种引用类型看这里)
因为如果这里使用普通的 key-value 形式来定义存储结构,实质上就会造成 Entry 的生命周期与线程强绑定,只要线程不死,那么这个线程里面的 ThreadLocalMap 中的 Entry 中的 key(就是 ThreadLocal)就会一直存在,在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。如果将 key (ThreadLocal)设置成被弱引用给引用了的话,那么这个 key 一般活不过下一次GC。随着 JVM 的下一次的 GC ,该 ThreadLocal 就会被回收,在 ThreadLocalMap 里对应的 Entry 的键值会失效,然后各种清理算法都是围绕着 k == null 作为条件进行清理的,这为ThreadLocalMap本身的垃圾清理提供了便利。
疑问:上面的意思就是每个线程中的 ThreadLocalMap 里面的 key 只能活过一次 GC 时间的话,那我假如还要使用这个 key 对应的 value 该怎么办呢?它都已经被 GC 了。
根据这一篇博客中的数据,下一次 GC 的时间大概至少离你开启这个线程有 2.6 秒,那时候你这个线程基本就执行结束了。
Reference.class
public T get() {
return this.referent;
}
private T referent; /* Treated specially by GC */
在分析 replaceStaleEntry ( key, value, i ) 之前,先看一下这个清理方法
/**
* Heuristically scan some cells looking for stale entries.
* This is invoked when either a new element is added, or
* another stale one has been expunged. It performs a
* logarithmic number of scans, as a balance between no
* scanning (fast but retains garbage) and a number of scans
* proportional to number of elements, that would find all
* garbage but would cause some insertions to take O(n) time.
*
* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
*
* @param n scan control: <tt>log2(n)</tt> cells are scanned,
* unless a stale entry is found, in which case
* <tt>log2(table.length)-1</tt> additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
*
* @return true if any stale entries have been removed.
*/
private boolean cleanSomeSlots(int i, int n) { // 上面传进来的是 i (int i = key.threadLocalHashCode & (len-1);)和 table 的 size (entry 的个数)
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) { // 如果向后遍历到的 entry 不为 null,但是 entry 的 key 为 null
n = len;
removed = true; // 但凡只要是清除了一个无效的 entry,removed 的值都会被该成 true
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0); // n = n + 1 ,然后再判断 n != 0
return removed;
}
接下来就详细的看一看,当线性探测到 k 为 null 的时候,这个 replaceStaleEntry ( ) 函数怎么操作的。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) { // staleSlot 就是传过来的这个索引位置的 entry 的 k 为 Null 的地方
Entry[] tab = table;
int len = tab.length;
Entry e;
// 向前扫描,查找最前的一个无效 slot,然后把那个位置的索引用 slotToExpunge 记录下来
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len)) {
if (e.get() == null) {
slotToExpunge = i; // 说实话,如果不出现错误,从 staleSlot 一直向前进行探测,由于是环形的 entry ,所以最前的一个无效 slot 应该就是 staleSlot
}
}
// 向后遍历 table
for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 找到了key,将最新的 value 值赋给这个 key,然后将这个这个索引位置的 entry 和 传进来的 staleSlot(也就是一开始的那个探测到的 key == null 的位置)位置的 entry 进行替换
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
/* 在从 staleSlot 向后扫描 的过程中找到了 key 的情况下:
* 如果之前的向前扫描的最前面的无效的 slot 的确是 staleSlot ,那么就从这个 找到 key 的 entry 的索引作为清理起点(一般是这样);但是如果之前的
* 向前扫描的最前面的无效的 slot 不是 staleSlot(一般不可能,除非见鬼了),那就以那个无效的 slot 位置作为清理起点
* 其实这个选取清理的起点考虑的还是比较全面的,因为它几乎是选取了整个 table 的最前面的一个无效的 slot 作为起点开始清理,*/
if (slotToExpunge == staleSlot) {
slotToExpunge = i;
}
// 从 slotToExpunge 开始做一次连续段的清理,再做一次启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果向后遍历 table 的过程中又遇到 key == null 的情况了,并且向前扫描过程中没有无效 slot,则更新 slotToExpunge 为当前位置
if (k == null && slotToExpunge == staleSlot) {
slotToExpunge = i;
}
}
// 如果向后遍历 table 的整个过程都结束了,还是没有找到 key,则在就在传进来的一开始的 key == null 的地方放一个 entry,
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 在探测过程中如果发现任何无效 slot(因为 slotToExpunge 没有回到刚开始的地方),则做一次清理(连续段清理+启发式清理)
if (slotToExpunge != staleSlot) {
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
}
回顾一下 ThreadLocal 的 set 方法可能会有的情况
探测过程中 slot 全都有效,并且顺利找到 key 所在的 slot,直接替换即可
探测过程中发现有无效 slot,调用 replaceStaleEntry,效果是最终一定会把 key 和 value 放在这个 slot 上,并且会尽可能清理无效 slot
在 replaceStaleEntry 过程中,如果找到了 key,则做一个 swap 把它放到那个无效 slot 中,value置为新值。然后 做一次清理(连续段清理+启发式清理)
在 replaceStaleEntry 过程中,没有找到 key,直接在无效 slot 原地放 entry 。然后 做一次清理(连续段清理+启发式清理)
如果上面的整个环形探测都是做完了既没有找到这个 key,也没有发现 k ==null 的;就把新的 entry 放到这个位置(这个位置就是每一个 ThreadLoca 的 ID int i = key.threadLocalHashCode & (len-1); ),table 中的 entry 的个数 + 1; 这也是线性探测法的一部分。放完后,做一次启发式清理,如果一个 key 都没有被清理出去,并且当前 table 大小已经超过阈值了,则做一次 rehash,rehash 函数会调用一次 slot,也即 expungeStaleEntries,如果完了之后 table 大小超过了 threshold - threshold / 4,则进行扩容 2 倍。
对上面用到的几个方法进行补充
/**
* 设置resize阈值以维持最坏2/3的装载因子
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 环形意义的下一个索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 环形意义的上一个索引
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
get()
再来看看 get 方法的源码:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
// 每个 threadLocal 都有自己独立的 threadLocalHashCode
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 对应的 entry 存在且未失效 且弱引用指向的 ThreadLocal 就是key,则命中返回
if (e != null && e.get() == key) {
return e;
} else {
// 因为用的是线性探测,所以往后找还是有可能能够找到目标 Entry 的。
return getEntryAfterMiss(key, i, e); // 这个传的参数要注意,它是以 table 中的 entry 为寻找目标的,而这个传进来的 e 并不是一定为 null 的,
} // 也有可能是 e 不为 null,但是 e.get()!= key 的
}
/*
* 调用getEntry未直接命中的时候调用此方法
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 基于线性探测法不断向后探测直到遇到空 entry。
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到目标
if (k == key) {
return e;
}
if (k == null) { // 本次查找的 entry 不为 null,但是 entry 中的 key 为 null
// 该 entry 对应的 ThreadLocal 已经被回收,调用 expungeStaleEntry 来清理无效的entry
expungeStaleEntry(i);
} else { // 本次查找的 entry 中的 k 与 key 不相同,而且本次的 entry 中的 k 也不为 null
// 取得新索,环形意义下往后面走
i = nextIndex(i, len);
}
e = tab[i]; // 拿到新的 entry,继续进行 while 的循环,一直到 entry == null 的时候结束 while 循环(因为 entry 的存储都是按照 nextIndex 得到的索引进行存储的)
} // 所以,一旦有一个为 null,那么后面的基本都是 null 了。
return null;
}
/**
* 这个函数是 ThreadLocal 中核心清理函数,它做的事情很简单:
* 就是从 staleSlot 开始遍历,将无效(弱引用指向对象被回收)清理,即对应 entry 中的 value 置为 null,将指向这个 entry 的 table[i] 置为 null,直到扫到空 entry。
* 另外,在过程中还会对非空的 entry 作 rehash。
* 可以说这个函数的作用就是从 staleSlot 开始清理连续段中的 slot(断开强引用,rehash slot等)
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 因为 entry 对应的 ThreadLocal 已经被回收,value 设为 null,显式断开强引用
tab[staleSlot].value = null;
// 显式设置该 entry 为 null,以便垃圾回收
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 清理对应 ThreadLocal 已经被回收的 entry
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
/*
* 对于还没有被回收的情况,需要做一次 rehash。
*
* 如果对应的 ThreadLocal 的 ID 对 len 取模出来的索引 h 不为当前位置i,
* 则从 h 向后线性探测到第一个空的 slot,把当前的 entry 给挪过去。
*/
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.
*
* 这段话提及了Knuth高德纳的著作TAOCP(《计算机程序设计艺术》)的6.4章节(散列)
* 中的R算法。R算法描述了如何从使用线性探测的散列表中删除一个元素。
* R算法维护了一个上次删除元素的index,当在非空连续段中扫到某个entry的哈希值取模后的索引
* 还没有遍历到时,会将该entry挪到index那个位置,并更新当前位置为新的index,
* 继续向后扫描直到遇到空的entry。
*
* ThreadLocalMap 因为使用了弱引用,所以其实每个 slot 的状态有三种也即
* 有效(value未回收),无效(value已回收),空(entry==null)。
* 正是因为ThreadLocalMap的entry有三种状态,所以不能完全套高德纳原书的R算法。
*
* 因为 expungeStaleEntry 函数在扫描过程中还会对无效slot清理将之转为空slot,
* 如果直接套用R算法,可能会出现具有相同哈希值的entry之间断开(中间有空entry)。
*/
while (tab[h] != null) {
h = nextIndex(h, len);
}
tab[h] = e;
}
}
}
// 返回staleSlot之后第一个空的slot索引
return i;
}