ThreadLocal笔记

笔记一:

 1 static class ThreadLocalMap {
 2 ...
 3  * The table, resized as necessary.
 4          * table.length MUST always be a power of two.
 5          */
 6         private Entry[] table;
 7 
 8         /**
 9          * The number of entries in the table.
10          */
11         private int size = 0;
12 
13         /**
14          * The next size value at which to resize.
15          */
16         private int threshold; // Default to 0
17 
18         /**
19          * Set the resize threshold to maintain at worst a 2/3 load factor.
20          */
21         private void setThreshold(int len) {
22             threshold = len * 2 / 3;
23         }
24 ...
25 }

这个  private int threshold;  属性意思是ThreadLocalMap内的Entry[]数组最大数据项 项数的意思。

由于 《Java数据结构和算法中文版 -- 第二版》 第11 章 哈希表 : 

  •  P415 聚集 
  • 数组填得越满,聚集越可能发生。数组有一半数据项时,这通常不是问题,当三分之二满的时候,情况也不会太坏。 然而,如果超过这个界限,随着聚集越来越严重,性能下降也很严重。因此,设计哈希表的关键是确保它不会超过整个数组容量的一半最多到三分之二(在本章最后将讨论哈希表的装填数据的程度和探测长度的数学关系。)
  • 装填因子(表中数据项数/表长)

笔记二:replaceStaleEntry()方法

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)]) {
            ThreadLocal<?> k = e.get();

            if (k == key) {
                //如果key是相同,则替换,并return
                e.value = value;
                return;
            }

            if (k == null) {
                //e!=null,key==null,因为key是弱引用,所以key已经被gc回收了,replaceStaleEntry方法就是用来解决内存泄露问题
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }


    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                   int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;

        int slotToExpunge = staleSlot;
        //prevIndex是指针向前,寻找前面过期数据
        for (int i = prevIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;

        //向后寻找key相同的数据
        for (int i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();


            if (k == key) {
                e.value = value;
                //通过和过期的slot进行交换,维护哈希表顺序
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                //清除过期slot
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }

             if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // 如果key并没有在map中出现过,则直接创建
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        //如果还有其他过期slot,则清除
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

set方法首先根据key(通过哈希函数)计算存储位置
如果计算出来的下标不为空,会进入循环,循环内如果key相同,则直接替换,如果key被回收,则调用replaceStaleEntry方法清除,并且在该方法中设置value。
如果计算出来的下标为空(说明哈希函数计算的下标值直接找到了该插入的位置),则直接设置值,并在最后通过cleanSomeSlots清除过期key和确定是否通过rehash扩容。
 

笔记三:replaceStaleEntry()方法

/** 先来看replaceStaleEntry 方法,该方法源码为: */
/*
 * @param  key the key
 * @param  value the value to be associated with key
 * @param  staleSlot index of the first stale entry encountered while
 *         searching for key.
 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).

    //向前找到第一个脏entry
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
1.          slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            /*更新*/
2.            e.value = value;
            /** 通过和过期的slot进行交换,维护哈希表顺序 */
            //如果在向后环形查找过程中发现key相同的entry就覆盖并且和脏entry进行交换
3.            tab[i] = tab[staleSlot];
4.            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            //如果在查找过程中还未发现脏entry,那么就以当前位置作为cleanSomeSlots
            //的起点
            if (slotToExpunge == staleSlot)
5.                slotToExpunge = i;
            //搜索脏entry并进行清理
6.            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        /**如果向前未搜索到脏entry,则在查找过程遇到脏entry的话,后面就以此时这个位置作为起点执行cleanSomeSlots*/
        if (k == null && slotToExpunge == staleSlot)
7.            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    /**如果在查找过程中没有找到可以覆盖的entry,
       则将新的entry插入在脏entry
       <---在set()方法中的线性探测发现了一个脏Entry,脏Entry的位置 本该是新数据项的位置。
       但是并不能保证该“脏Entry”数据项后面的Entry.referent对象 不与 待插入的对象 相等
       如果脏Entry后,又出现了 和待插入数据项 相等 的 ThreadLocal类型的Entry.referent对象,
       那么 需要交换它们的位置(原理文章后面最后一个注释)*/
8.    tab[staleSlot].value = null;
9.    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
10.    if (slotToExpunge != staleSlot)
        //执行cleanSomeSlots
11.        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

该方法的逻辑请看注释,下面我结合各种情况详细说一下该方法的执行过程。首先先看这一部分的代码:

int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
这部分代码通过PreIndex方法实现往前环形搜索脏entry 的功能,初始时slotToExpunge和staleSlot相同,若在搜索过程中发现了脏entry,则更新slotToExpunge为当前索引i。另外,说明replaceStaleEntry并不仅仅局限于处理当前已知的脏entry,它认为在出现脏entry的相邻位置也有很大概率出现脏entry,
所以为了一次处理到位,就需要向前环形搜索,找到前面的脏entry




以下为我的注释:
注: 脏entry   的条件如下:
Entry e!=null && ThreadLocal<?> k = e.get() 的 k == null
因为Entry类对象是 持有实例referent 为 ThreadLocal<?>类型的WeakReference对象
所以key已经被gc回收了,replaceStaleEntry方法就是用来解决内存泄露问题。





注:向前环形搜索
所用的方法如下:
private static int prevIndex(int i, int len) {
     /**wrap around if necessary*/
     return ((i - 1 >= 0) ? i - 1 : len - 1);    
}
用一个if语句来检查这种情况,要i-1值小于等于0,就把它的值设置为“整个数组长度-1”。这个方法保障了ThreadLocalMap内的Entry数组的整个长度范围内都被搜索到。
 





请仔细看ThreadLocalMap.set()方法的这部分代码:
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
对于条件 if(k==key),只检索了整个数组长度的前一部分。

那么,如果数组剩余部分的数据项出现了和待插入的ThreadLocal<?> key相等的情况该怎么办?

所以,在ThreadLocalMap.replaceStaleEntry()方法中,这一块代码逻辑会检索整个数组长度的剩余后面的部分。
    for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
 
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
 
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }






ThreadLocalMap类的replaceStaleEntry()方法内,我添加的注释写到 “通过和过期的slot进行交换,维护哈希表顺序”。

 原理参考:《Java数据结构和算法第二版》P414
第十一章 哈希表 

小结:开放地址法
    次小结:线性探测
Find按钮
根据冲突的位置,查找算法只是沿着数组一个一个地察看每个单元。
如果在找到要寻找的关键字前遇到一个空位,说明查找失败。
不需要再做查找,因为插入算法本应该把这个数据项都插在那个空位上。

ThreadLocalMap.set()方法中,首先通过哈希函数计算出要插入的ThreadLocal<?> key对象的位置,从这个位置开始“线性探测”的察看顺延的ThreadLocalMap.Entry[] table 这个数组各个数据项内容。
如果发现要插入的对象和ThreadLocalMap.Entry数组内的Entry.referent (hreadLocal<?>类型)相等,那么更新ThreadLocalMap.Entry.value 的值。

如果发现ThreadLocalMap.Entry数组内的Entry.referent值为null,而数组内该Entry数据项不为空,(这种情况简称为“脏Entry”)那么需要调用replaceStaleEntry()进行处理。

ThreadLocalMap.replaceStaleEntry()方法中,从脏Entry的下一位数据项开始查:

如果发现在以后的数据项中,遇到了和待插入的ThreadLocal<?> key对象 相等的数据项。那么进行如下处理:

把后面的那个和“待插入的ThreadLocal<?> key对象 相等的数据项” 与 “脏Entry”交换。

此时,就维护了哈希表的顺序。原理见上

笔记四: cleanSomeSlots()方法

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