ThreadLocal 难点解析

一般性的介绍(内存泄漏,基本用法,应用场景,最佳实践等)官网和其他博客都说的很清楚,这里主要记录一下我认为threadlocal的最核心的地方和难点。

主要会包括以下方面:1. 内存泄漏问题,对象引用关系 2. threadLocalHashCode值的选取 3. 深入探究set方法。

注:本文中代码选自jdk8。

其实这两个问题,乃至其他的问题我认为都是对象引用关系,这个是最核心的。

1. 内存泄漏问题,对象引用关系

ThreadLocal 难点解析_第1张图片

这个图引用自(https://mp.weixin.qq.com/s/bxIkMaCQ0PriZtSWT8wrXw##)架构师修炼宝典 这个公众号。

上图已经将引用关系讲的十分清楚了,由于每个线程对他自己的ThreadLocal持有的引用是放在线程私有的栈里面的,那一旦这个线程丢失了对自己的ThreadLocal的引用之后,如果gc了,那么这个线程里面的ThreadLocalMap里面的对应的Entry的key对ThreadLocal的weakReference将会被回收,那么这里的value值将会变成不可达状态,如果这个entry没有及时被remove掉,那么就会导致内存泄漏问题。

2. threadLocalHashCode值的选取

先说说这个是什么 看代码:

    /**
     * Get the entry associated with key.  This method
     * itself handles only the fast path: a direct hit of existing
     * key. It otherwise relays to getEntryAfterMiss.  This is
     * designed to maximize performance for direct hits, in part
     * by making this method readily inlinable.
     *
     * @param  key the thread local object
     * @return the entry associated with key, or null if no such
     */
    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);
    }

实际上就是每个threadlocal对应的hashCode!看看他的生成原理:

private final int threadLocalHashCode = nextHashCode();

继续

/**
 * The difference between successively generated hash codes - turns
 * implicit sequential thread-local IDs into near-optimally spread
 * multiplicative hash values for power-of-two-sized tables.
 */
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * Returns the next hash code.
 */
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

就是每个threadlocal的hash值就是i*HASH_INCREMENT+HASH_INCREMENT;

i为第i个出引用这个threadLocalHashCode的线程。

注意这里是final修饰的threadLocalHashCode值,每个线程的threadlocal只会有一次赋值的机会。

这个数是魔数,由于每一次扩容都是2的整数次方,用这个算法产生的hash值能够均匀地落到数组的每一个小格子里面,也就是说如果正常流程添加满,是不会冲突的,是可以达到空间效益最大化并且减少hash冲突的。(当然不可能会有加满的时刻,threadlocalMap在被加满前会扩容的)

(https://mp.weixin.qq.com/s/bxIkMaCQ0PriZtSWT8wrXw##)

3.深入探究set方法。

在我们看最核心的代码,set方法,前先看看简单的get,最终get会调用set的。

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
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();
}

这里的getMaps一开始我还以为是把t作为key值去map里面查,实际上不是的,是返回当前thread持有的threadlocalmap。

    /**
     * Get the entry associated with key.  This method
     * itself handles only the fast path: a direct hit of existing
     * key. It otherwise relays to getEntryAfterMiss.  This is
     * designed to maximize performance for direct hits, in part
     * by making this method readily inlinable.
     *
     * @param  key the thread local object
     * @return the entry associated with key, or null if no such
     */
    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);
    }

这里的get就是直接算出hashcode和len-1的与,然后去找到中国entry取值,getEntryAfterMiss这个分支我们先不看。我们先看上面的setInitialValue。

/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */
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;
}

这里有一个小地方,就是初始化方法是在第一次调用get的时候触发。
然后我们接着看TheadLocal的内部类ThreadLocalMap.set。

        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
    private void set(ThreadLocal key, Object value) {

        // We don't use a fast path as with get() because it is at
        // least as common to use set() to create new entries as
        // it is to replace existing ones, in which case, a fast
        // path would fail more often than not.

        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;
            }
            //走到以上都是常规逻辑,走到这里就有点奇怪了。
            /*其实这里就是为了解决内存泄漏问题的,当走到k==null且entry非空的地方就意味着,弱引用被gc了。因此这个staleentry需要被替换掉。*/
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

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

继续看replaceStaleEntry

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

        int slotToExpunge = staleSlot;
        //循环往前找需要被替换掉的staleslot,直到空为止,然后将最前面的i值赋值给slotToExpunge,表不可能满,肯定不能走完一个循环,下面的同理
        for (int i = prevIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;
        
        //循环往后遍历这个数组
        for (int i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal k = e.get();

            if (k == key) {
                e.value = value;
                
                //找到相同的key之后,交换掉i和staleSlot,之后i就是坏节点,是需要被擦除的                
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;
                
                /*如果只有staleSlot前面没有节点,那就把i擦除掉,为什么要这样,可能是因为staleSlot是冲突之后偏移更加短的值,会更加接近真实hash值*/

                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }

            /*如果这个k为空的话把这个节点变成坏节点,为什么要这样,因为staleSlot要么会拿来交换,要么就会拿来建新值,终究会变成好人,但是这个铁定是坑爹的,如果交换了之后,在下面的cleanSomeSlots之后不久交换后的坑爹也会被删掉*/
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // 如果找不到相同的key,那就直接赋值
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        // 如果这两个值不等才去删除,因为这样就找到了坑爹货,staleSlot到了这一步都是好人
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

看expungeStaleEntry

    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // 能进来的这个方法的staleSlot都铁定是坑爹的。
        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) {
                e.value = null;
                tab[i] = null;
                size--;
        //一直对当前的entry里面的key进行修正位置,(有可能之前的线性探测之后,位置偏移过多,优化表),这里总会遇到空的, 因为表不可能满,会扩容。
            } 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;
    }

最后我们看cleanSomeSlot

    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            //下面的代码是清除操作e.get获取的是key值,也就是threadlocal
            i = nextIndex(i, len);
            Entry e = tab[i];
            if (e != null && e.get() == null) {
                n = len;
                removed = true;
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }

这个方法一开始看是不太好理解为什么要循环这个次数:(n >>>= 1) != 0。实际上代码注释也说了,这个是一个tradeoff,可以理解为预期。如果遇到的空值了,理解为可能内存泄漏情况严重,而且刚gc过,那就是继续加大预期继续清除。

回过头来看我们之前忽略过的一个方法getEntryAfterMiss,这个是在一开始key匹配不上的时候调用的。

    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)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

这个方法其实中间这里会调用expungeStaleEntry这个东西,我们也知道这个东西里面会清除掉当前slot的entry,并且还会做一个循环遍历的rehash操作,那会不会发生那种原本能找到这个key,但是却在rehash之后找不到的情况呢??我一开始还有这个担心,但是实际上肯定也不会的。

因为这里rehash只会去占用null的entry,那假如我们在expungeStaleEntry里面恰好遇到了合适的key,那结论就是当前的e就是我们要找的那个entry,因为在循环体里面k==null的时候i是不会步进的,而且i之前和这个key对应的hash之后的这一段是被证明过entry非空的!

然后还有一个问题是,这个get能够完全保证能找到这个合适的key吗,中间会不会有null之类的事情发生?我觉得应该是可以的,因为就算内存泄漏了,但是entry还是非空,而且几乎每个重要操作都有清除stale entry和rehash的操作,应该是足够健壮的!

转载于:https://www.cnblogs.com/kobebyrant/p/11229626.html

你可能感兴趣的:(java)