Java 8 ThreadLocal 源码解析

Java 8 ThreadLocal 源码解析

原文几处备注如下:

   

  1. 原文写到:

    源码解析

    我们从ThreadLocal.set方法开始分析:
    ....

    每个 Thread 对象维护了一个 ThreadLocalMap 类型的 threadLocals 字段。

    ThreadLocalMap 的 key 是 ThreadLocal 对象, 值则是变量的副本, 因此允许一个线程绑定多个 ThreadLocal 对象

     

    这里我自己解释下:

    ThreadLocalMap内部维护了一个名为table的 Entry[]数组实例变量,而Entry类是定义在ThreadLocalMap类内部的静态实例内部类。Entry类的键(Key)是ThreadLocal,值(Value)是Object类型。所以说 因此允许一个线程绑定多个 ThreadLocal 对象
  2. 原文写到

    为了处理GC造成的空洞(stale entry), 需要调用expungeStaleEntry方法进行清理。

此处的空洞(stale entry)相当于脏entry,具体情况如下:

Entry e!=null && ThreadLocal<?> k = e.get() 中的 k == null。
因为Entry类对象是 持有实例referent 为 ThreadLocal<?>类型的WeakReference对象
所以key已经被gc回收了,replaceStaleEntry方法就是用来解决内存泄露问题。
expungeStaleEntry()方法就是:
清理当前传入参数下标的脏Entry,然后把当前脏Entry 顺延线性探测到 下一个空Entry空Entry : Entry== null) 之间的 所有脏Entry 都清理掉。且把这个顺延探测途中的普通Entry放到最前的哈希槽(哈希槽”解释: 它是ThreadLocal类中的静态实例变量ThreadLocalMap里面的 名为table的私有实例的一个数据项。这个名为table的私有实例---它的类型是Entry数组)中---维护哈希顺序。
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 清理当前的空洞
    tab[staleSlot].value = 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();
        // 若下一个位置还是空洞将其一并清除
        if (k == null) { //hit it !
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 发现不是空洞的 Entry 将其放入最靠前的哈希槽中
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {               //h != i说明有过hash冲突 --- 备注【1】
                tab[i] = null;
                while (tab[h] != null) // 处理移动过程中的哈希冲突
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
     /**循环执行直到遇到空的哈希槽, 表明从 staleSlot 开始的查找序列中间(从 staleSlot 开始的聚集或者从 staleSlot 开始的一次探测)不会存在空哈希槽或空Entry*/
    }
    return i;
}

备注【1】:

Java数据结构与算法第二版 P412

冲突:

不可避免把几个不同的单词哈希化到同一个数组单元,至少偶尔会这样。当然希望每个数组下标对应一个数据项,但是通常这不可能。只能寄希望于没有太多的单词有同样的数组下标。

假设要在数组中插入单词melioration。通过哈希函数得到了它的数组下标后,发现那个单元已经有了一个单词(demystify)了,因为这个单词哈希化后得到的数组下标与melioration相同(对一个特定大小的数组)。这种情况称为冲突。

  • 注意看JDK关于 ThreadLocalMap类的expungeStaleEntry()方法源码的注释如下(见红字标注):

    3.原文写道;

 

查看代码


/**
      * Version of getEntry method for use when key is not found in
      * its direct hash slot.
      *
      * @param  key the thread local object
      * @param  i the table index for key's hash code
      * @param  e the entry at table[i]
      * @return the entry associated with key, or null if no such
  */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {  /**3 哈希槽中为null, 说明搜索结束未找到目标*/
        ThreadLocal<?> k = e.get();
        if (k == key)	/**1 哈希槽中是当前ThreadLocal, 说明找到了目标*/
            return e;
        if (k == null) 
                   /**4 哈希槽中存在Entry, 但是 Entry 中没有 ThreadLocal 对象。因为 Entry 使用弱引用, 这种情况说明 ThreadLocal 被GC回收。所以,将过期的key清除掉,并把后面的元素(移动过位置的)往前移*/
            expungeStaleEntry(i);
        else                     /**2 哈希槽中为其它ThreadLocal, 需要继续查找 */
            i = nextIndex(i, len);   /**往后移一位**/
        e = tab[i];
    }
    return null;      /**3 哈希槽中为null, 说明搜索结束未找到目标*/
}

getEntryAfterMiss 方法会循环查找直到找到或遍历所有可能的哈希槽, 在循环过程中可能遇到4种情况:

  • 1 哈希槽中是当前ThreadLocal, 说明找到了目标
  • 2 哈希槽中为其它ThreadLocal, 需要继续查找
  • 3 哈希槽中为null, 说明搜索结束未找到目标
  • 4 哈希槽中存在Entry, 但是 Entry 中没有 ThreadLocal 对象。因为 Entry 使用弱引用, 这种情况说明 ThreadLocal 被GC回收。

这里解释一下为什么“哈希槽中为null,说明搜索结束”---参考 Java数据结构和算法中文版(第二版)P414
Find按钮

Find按钮对键入的关键字值应用哈希函数。结果是一个数组下标。这个下标所指单元可能是要寻找的关键字;这是最好的情况,并且立即报告查找成功。

然而,也可能这个单元被其他关键字占据。这就是冲突;会看到红色箭头指向被占用的单元。根据冲突的位置,查找算法依次查找下一个单元。查找合适单元的过程叫做探测。

根据冲突的位置,查找算法只是沿着数组一个一个地察看每个单元。如果在找到要寻找的关键字前遇到一个空位,说明查找失败。不需要再做查找,因为插入算法本应该把这个数据项插在那个空位上(如果不是前面那样的话)。图11.7显示了成功和不成功的线性探测。






其他参考文章:
Java多线程与并发之ThreadLocal

posted @ 2021-12-16 21:32  王超_cc  阅读(171)  评论(0编辑  收藏  举报