ThreadLocal 源码分析

前言快速到底

  由于 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(); // 开启五个线程
        }
    }

}

注:

  initialValue() 方法在调用 get() 方法的时候会第一次调用,但是如果一开始就调用了set() 方法,则该方法不会被调用。通常该方法只会被调用一次,除非手动调用了 remove() 方法之后又调用 get() 方法,这种情况下,get() 方法中还是会调用 initialValue() 方法。该函数是 protected 类型的,很显然是建议在子类重载该函数的,所以通常该函数都会以匿名内部类的形式被重写,以指定初始值。

运行结果

线程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;
}

 

 

 

 

 

 

 

 

 

 

 

转载于:https://www.cnblogs.com/tkzL/p/8796995.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值