ThreadLocalの難しさの解析

一般的な導入(メモリリーク、基本的な使用方法、アプリケーションのシナリオ、ベストプラクティスなど)や他のブログの公式ウェブサイトは非常に明確に、これは場所に関するレコードが主であると私はコアのThreadLocalの難しさを考えると述べました。

1人のメモリリークの問題では、深さ照会セット法3のオブジェクトの参照関係2. threadLocalHashCode値を選択します。それは主に以下を含むであろう。

注:このコードは、選択jdk8を。

実際には、これらの2つの問題だけでなく、他の問題は、私は、これはコアオブジェクトの参照関係であると考えています。

1.メモリリーク、オブジェクトの参照関係

この図は、この公開番号(https://mp.weixin.qq.com/s/bxIkMaCQ0PriZtSWT8wrXw##)建築家のコレクションの練習から引用します。

彼自身の保有の各スレッドは、内部のプライベート・スタックスレッドのThreadLocal引用された上で、その一度このスレッドがあれば、後に彼自身のThreadLocalへの参照が不足しているので、図は、非常に明確な関係の話を引用されていますgcは、その後のThreadLocalの弱い参照の内部スレッド内のエントリをThreadLocalMap対応するキーが回収され、この値は値が到達不能状態になったことになるエントリがタイムリーに出て削除されていない場合、それはなりますメモリリークの問題が発生します。

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をのハッシュ値iがHASH_INCREMENT + HASH_INCREMENTを*です。

私はこのthreadLocalHashCodeスレッドi番目の参照です。

これは最終的な修飾threadLocalHashCode値であることに注意し、各スレッドはThreadLocalの割り当ての機会を有することになります。

このマジックナンバーは、各時間拡大は2の整数乗であるので、このアルゴリズムによって生成されたハッシュ値が均一内の各落下小さな正方形のアレイとすることができ、数であり、それは完全な正常なプロセスが追加された場合と言うことではなかったです競合がスペース効率を最大化し、ハッシュ衝突を減らすために達成することができます。(もちろん、満たされる前threadlocalMapが拡大されます、時間を埋めるためには不可能があるでしょう)

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

3.イン深度問い合わせセット方法。

我々は最終的に、コールセットを取得する、シンプルなGETを初めて目の前に、コードのコア、設定方法を見てください。

/**
 * 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値だと思った、実際には、現在のスレッドが保持している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);
    }

ここに来る直接getEntryAfterMissは、私たちがこのブランチを見ていない、ハッシュコードとlen-1を計算して、中国のエントリ値を探しに行くれます。私たちは、上記の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;
}

最初の呼び出しがで取得する際にここで小さな場所ですが、それがトリガされたメソッドを初期化することです。
その後、我々は内部クラスThreadLocalMap.set TheadLocalを見に行きました。

        /**
         * 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!予想通り、また前記実際のコードのコメントでは、これはトレードオフであり、理解することができます。あなたがNULL値に遭遇した場合、それは深刻な状態でメモリリークとして理解され、あまりにもGC、およびそれは、クリアランスを増加し続けるために継続すると予想されていてもよいです。

おすすめ

転載: www.cnblogs.com/kobebyrant/p/11229626.html