JDK源码系列:ThreadLocalMap到底是如何清理失效节点的?

020d84227a867f99bcdfe9e4dab40773.jpeg

在前面的关于ThreadLocal的文章中提到了所谓的内存泄漏问题,同时也提到了ThreadLocalMap在某些场景下会主动清理坏掉的Entry来释放内存,要理解它是怎么做到的,就必须理解它是怎么解决哈希冲突的,尤其是“真删除”Entry后如何保证不影响后续Entry的查找问题

一、哈希冲突的解决:线性探测法

在线性探测法中插入和查找和修改都比较容易理解,难点在于删除动作,因为删除动作可能会影响后续节点查找的正确性,在业界有两种做法:

一种是假删除,在Entry上增加状态属性,将状态置为删除,优点是不需要移动后续Entry,缺点是不能完全释放Entry,同时在插入和查询时要增加状态判断逻辑,数据不够紧凑影响查找效率;如下:key3、key4、key5发生哈希冲突,删除key3节点,只需要把状态改为status_del即可,这样不会影响key4和key5的查询,把status_del当成存在的节点即可。

e94b08351846f9904cd7c157b4160f73.png

另一种是真删除,将Entry释放回收,同时移动后续Entry到正确的位置,优点就是数据紧凑查找效率高,缺点是影响删除的效率;

如下:key3、key4、key5发生哈希冲突,删除key3节点后,它的后续节点(key4,key5,key1)需要rehash,最终重新分布如下。

f39ac4fe8938c373a717790eabc47364.png

f8c741d44ad110eb22485f84b7430720.png

在ThreadLocalMap中使用的算法是“真删除”。

二、如何何时清理坏掉的Entry?

在get\set\remove方法中某些场景下会进行局部清理操作释放内存,在扩容之前也会进行全局清理操作(扩容方法在set方法中某种场景下调用),要想彻底弄明白还不是那么容易,下面是我对ThreadLocalMap的源码注释,感兴趣的同学可以瞧瞧。

public class ThreadLocal<T> {
    /**
     * 为了让哈希码能均匀的分布在2的N次方的数组里
     */
    private static final int HASH_INCREMENT = 0x61c88647;
    /**
     * 用来辅助生成哈希码的
     */
    private static AtomicInteger nextHashCode =
            new AtomicInteger();
    /**
     * ThreadLocal 实例作为ThreadLocalMap的key,它的哈希码来自于此处
     */
    private final int threadLocalHashCode = nextHashCode();


    /**
     * 生成哈希码的方法
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }


    /**
     * 创建可继承的ThreadLocalMap
     * 在Thread构造方法中可能调用此方法
     */
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }


    /**
     * 获取当前ThreadLocal实例关联当前线程的value
     * 如果ThreadLocalMap还没有创建则会进行创建,并初始化节点值
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T) e.value;
                return result;
            }
        }
        return setInitialValue();
    }


    /**
     * ThreadLocalMap初始化
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }


    /**
     * 设置值,如果map不存在则初始化map,并初始化节点值
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }


    /**
     * 移除当前ThreadLocal实例关联的Entry
     * 如果移除后调用get方法会导致Entry重新初始化,initialValue也会再次被执行。
     * 除非在调用get之前调用了set操作(Entry仍然需要初始化,但initialValue不会执行)。
     */
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }


    /**
     * 可以看出ThreadLocalMap是直接挂在Thread对象上的
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }


    /**
     * 创建ThreadLocalMap并初始化第一个节点值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }


    /**
     * InheritableThreadLocal中会覆盖此方法
     */
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }


    /**
     * 获取初始化值的另一种方式
     */
    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();
        }
    }


    /**
     * ThreadLocalMap是一个为了维护线程本地值对象而定制的一个哈希map
     * ThreadLocalMap访问权限是package级别
     * 为了有助于及时清理又大存活时间又长的对象,对Entry中的key使用了弱引用
     * 在扩容的情况下会清理过时的条目
     */
    static class ThreadLocalMap {
        /**
         * 初始容量 --必须是2的N次方
         */
        private static final int INITIAL_CAPACITY = 16;
        /**
         * 数组,必要情况下可扩容
         * 数组长度必须是2的N次方
         */
        private Entry[] table;
        /**
         * 数组中Entry的数量
         */
        private int size = 0;
        /**
         * 下次扩容的阈值
         */
        private int threshold; // Default to 0


        /**
         * ThreadLocalMap初始化:数组初始化,firstKey数组下标计算,节点Entry初始化,Entry数量赋值,下次扩容阈值计算
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }


        /**
         * 继承父线程的ThreadLocalMap(深克隆)
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];


            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }


        /**
         * 环形数组下标计算(下一个数组元素下标)
         */
        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);
        }


        /**
         * 扩容阈值的计算
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }


        /**
         * 获取Entry
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                //命中直接返回
                return e;
            else
                //存在哈希冲突
                return getEntryAfterMiss(key, i, e);
        }


        /**
         * 存在哈希冲突情况下取值,线性探测法
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;


            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    //命中
                    return e;
                if (k == null)
                    //清理过时Entry
                    expungeStaleEntry(i);
                else
                    //线性探测:下一个元素
                    i = nextIndex(i, len);
                e = tab[i];
            }
            //直到下一个元素为null,如果还没找到则返回null
            return null;
        }


        /**
         * 设值
         */
        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) {
                    //命中设值返回
                    e.value = value;
                    return;
                }


                if (k == null) {
                    //发现一个失效条目,则会进行复杂的新旧替换操作
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //新条目放到新的空位上
            tab[i] = new Entry(key, value);
            //条目数量加1
            int sz = ++size;
            //清理有限范围内的失效条目,如果找到了失效条目肯定会清理掉同时size--,这时一定不需要扩容。
            //如果没有清理任何失效条目,则size是可能达到阈值的,达到阈值则扩容。
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }


        /**
         * 删除 key对应条目
         */
        private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();//清掉key的弱引用
                    expungeStaleEntry(i);//清理i位置的条目,后续条目rehash,直到遇到null
                    return;
                }
            }
        }


        /**
         * 1、方法逻辑:先从当前位置A(过期的entry)向前遍历,直到遇到空entry,如果遇到过期entry,
         * 则记录下标(指向距离null最近的失效条目),标记为清理开始位置。
         * 从位置A向后遍历,直到遇到空entry,如果遇到key与当前ThreadLocal相等,那么填入value,
         * 再把当前entry与位置A的entry换位,然后先执行expungeStaleEntry,再执行cleanSomeSlots清理过期entry;
         * 如果没有找到key,那么就是遇到空entry了,直接new Entry并填入到位置A,然后同样先执行expungeStaleEntry,
         * 再执行cleanSomeSlots方法清理。
         * 2、如果不考虑清理的事情,只保证正确性的简单逻辑:向后遍历,在找到null节点之前,如果找到了key的旧节点,则填入value,
         * 并与staleSlot进行指针交换(保证正确的核心逻辑);如果没找到key的旧节点则说明这是一个新key,直接放到staleSlot位置即可
         * 3、核心思想:共3个指针,slotToExpunge指向最左侧失效节点,staleSlot始终指向中间位置不变,
         * i可能会指向右侧失效节点,i作为辅助指针,某种场景下slotToExpunge会指向i的位置,
         * 不管slotToExpunge在哪,它始终是清理的起始位置,从左向右进行启发式清理。
         * 清理不是必须的,staleSlot与i的位置交换才是核心,它保证了正确性,当这是一个新key时,就不会有交换发生。
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            int slotToExpunge = staleSlot;
            //向左查找失效节点,直到遇到null节点,用slotToExpunge记录最左侧失效节点的位置
            //如下:slotToExpunge从begin位置开始向左找,最后会指向stale2
            //null<-normal<-stale2<-normal<-stale1<-normal<-begin
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;
            //向后查找key所在节点(可能不存在))或者null节点
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //找到了key所在节点,和staleSlot交换位置
                if (k == key) {
                    e.value = value;
                    //和staleSlot交换位置,staleSlot指向了好的节点,i指向了坏的节点
                    //交换完成后就不会存在错误了,后面的清理就是锦上添花的事情
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;


                    // Start expunge at preceding stale entry if it exists
                    //staleSlot指向的已经不是失效条目,所以目前是最左侧失效条目是slotToExpunge
                    //最右侧失效条目是i,如果slotToExpunge一直没动的话,也就是左侧没有失效条目,
                    //这时将slotToExpunge指向i才是正确的,也就是左右指针重叠
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //清理slotToExpunge指向的条目,并且进行"启发式清理"
                    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.
                //右侧找到了失效条目,并且左侧没有找到失效条目,这时就将左侧指针指向右侧
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }


            //没找到旧key说明这是一个新key,直接占用到staleSlot位置即可
            //(这种情况下不清理任何失效节点也不会出错,后面的清理就是锦上添花的事情了)
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);


            // If there are any other stale entries in run, expunge them
            //slotToExpunge发生了移动,说明找到了失效条目,则进行启发式清理
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }


        /**
         * 删除一个指定位置的失效条目,并且重新hash后面的条目,直到遇到一个null条目。
         * 基于 "哈希冲突线性探测法的真删除原理"
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;


            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;


            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    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;
        }


        /**
         * 主要作用:
         * 从i位置开始搜索,如果在log2n个连续节点内发现了失效条目(key=null,value<>null)则进行清理失效条目,
         * 并且重置n的值为len,如此循环,直到连续log2n个节点都是正常节点时才会跳出while。
         * (和expungeStaleEntry(i)的区别就是它的清理范围更长,只要在log2n个连续节点有失效条目,
         * 那么你的清理范围就可以一直延长,极端情况下可能会清理整个数组,也可能没有清理掉任何节点)
         */
        private boolean cleanSomeSlots(int i, int n) {
            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) {
                    //发现失效条目就清理,并且重置n的值,这样就可以继续向后查找最多log2n个节点,可循环
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ((n >>>= 1) != 0);//不断的无符号右移,直到n = 0。如果连续log2n个节点都是正常节点则跳出循环
            return removed;
        }


        /**
         * 重新哈希所有条目:1、清理失效的条目 2、如果清理后还是达到了扩容阈值,则进行翻倍扩容
         */
        private void rehash() {
            expungeStaleEntries();


            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }


        /**
         * 数组容量翻倍
         */
        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;


            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }


            setThreshold(newLen);
            size = count;
            table = newTab;
        }


        /**
         * 删除数组中所有失效条目:遵守线性探测法删除原理
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }


        /**
         * Entry节点
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;


            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

三、总结

1、ThreadLocalMap使用了“线性探测法”来解决哈希冲突;

2、ThreadLocalMap使用了“真删除”来去除正常remove的节点或者坏掉的节点(key引用为null);

3、ThreadLocalMap真删除坏掉的节点后,会rehash后面的节点,直到遇到null  Entry为止,这就是expungeStaleEntry(i)的逻辑;

4、get操作中,如果遇到了哈希冲突,就会进行线性探测法查找,过程中遇到了失效节点就会发生expungeStaleEntry(j)清理,直到遇到null节点终止;

5、set操作中,某些场景下会进行复杂的replaceStaleEntry(key, value, i)和cleanSomeSlots(expungeStaleEntry(slotToExpunge), len)释放内存操作.

简单理解就是将新值放到“最优”的位置,方便高效查询,如果是一个新增操作就直接放在最优位置了,如果是一个更新操作,就需要找到“旧值”所在的位置,并进行“交换”操作,这就可以保证最基本的正确性了(map中不能存在两个相同key的条目)。

至于清理操作则是锦上添花的事情了,一方面为了及时清理失效节点释放内存,另一方面可以避免不必要的扩容。

在没有回收且占用了新的null节点场景下set后 size++很可能达到扩容的阈值,这时候会进行局部清理操作(cleanSomeSlots)后可能会size--,这就避免了扩容操作。

如果没有回收任何失效的条目则会进行全局rehash操作,在rehash操作中会先进行全局回收(遍历所有条目),如果size还是超越了阈值则会进行翻倍扩容操作;

6、remove操作中,会先找到对应Entry,然后回收key的指针,最后进行expungeStaleEntry(i)清理操作;

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吕哥架构

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值