[Great ThreadLocal] 1.ソースコード分析

1.はじめに

JDKでは、いくつかの目立たないクラスに大きなエネルギーが含まれていることがよくあります。ThreadLocalはそのようなクラスです。JDK1.2が生まれ、JDKのベテランと見なすことができます。この記事の最初から、家主は3つのパートでThreadLocalの知識を説明するつもりです。ソースコード分析からオープンソースプロジェクトThreadLocalのアプリケーション、およびThreadLocalの最も価値のある分散リンクトラッキングに至るまで、ThreadLocalの魅力を徐々に示すことが期待されます。以下は、記事のタイトルです。最初にフラグを設定し、次に穴を埋めます。

  • [Great ThreadLocal] 1.ソースコード分析
  • [Great ThreadLocal]第二に、オープンソースプロジェクトのThreadLocal戦闘アプリケーション
  • [Great ThreadLocal] 3番目に、分散リンクトラッキング

2、ThreadLocalデータモデル

分析ThreadLocalではなく、オープンの周りThreadThreadLocalMapThreadLocalMap.Entry鉄の三角形は、これらの三つのレベルとの関係整理はThreadLocalを突破するための鍵です。写真は上記の千の言葉の価値があります!

  • ThreadオブジェクトはthreadLocals属性を保持し、そのタイプはThreadLocalMapでありThreadLocal、静的内部クラスです。
  • ThreadLocalMap内部的にThreadLocaMap.Entry[]配列を維持します。ここで、項目のキーはThreadLocalタイプであり、値はクライアントによって設定されたオブジェクトです。
  • エントリのキーはThreadLocalオブジェクトを参照しています。この参照は特別です。弱い参照が使用されます。WeakReference

弱い参照(WeakReference):弱い参照に関連付けられたオブジェクト。GCが発生すると、メモリ不足に関係なく、弱い参照が指すオブジェクトはリサイクルされます。

ここに画像の説明を挿入

Q:スレッドにThreadLocalMapなどのマップ構造があるのはなぜですか?

A:この設計は、次のアプリケーションシナリオに示すように、スレッドが複数のThreadLocalオブジェクトを格納できるようにするためのものです。

private static ThreadLocal<Integer> threadLocal_int = new ThreadLocal<>();

private static ThreadLocal<String> threadLocal_string = new ThreadLocal<>();

// threadLocal_int 和 threadLocal_int 具有不同的 threadLocalHashCode,故123和abc存放在同一个线程的ThreadLocalMap的2个Entry中
public void test() {
    threadLocal_int.set(123);
    threadLocal_string.set("abc");
}

3、メモリリーク

ThreadLocalをマスターしたかどうかを判断するには、2つの質問を確認できます。

  • エントリのThreadLocalへの参照が弱い参照として設計されているのはなぜですか?
  • ThreadLocalにはメモリリークの問題がありますか?

最初に質問に答えるのではなく、これら2つの質問を分析して、分析から回答を導き出します。

3.1強い参照にはメモリリークがありますか?

別の角度から考えてみてください、もしそれが強い参照であるなら、何が問題でしょうか?ThreadLocalには、参照を保持する2つのソースがあります:A.クライアントが保持するThreadLocal_ref、この参照クライアントは割り当てを操作できます; B.スレッド内に保持されるThreadLocalMap.Entryのキーからの参照、この参照クライアントは完全に認識していません。クライアントがThreadLocalへの参照をアクティブに解放する(たとえば、割り当てを介してThreadLoca_ref= null;と想定されますが、Bソースからの強い参照がまだあるため、ThreadLocalオブジェクトはGCによってリサイクルできないため、ThreadLocalオブジェクトにはメモリリークのリスクがあります。

結論:強い参照にはメモリリークのリスクがある

3.2弱い参照にはメモリリークはありませんか?

上記の分析は、強い参照でメモリリークの可能性があることを示しています。弱い参照でメモリリークのリスクはありますか?画像で分析してみましょう:クライアントがThreadLocalオブジェクトへの強い参照を(割り当てによってThreadLoca_ref = null;アクティブに解放すると、GCこれが発生すると、Entry.keyはThreadLocalへの弱い参照であるため、ThreadLocalオブジェクトはリサイクルされますが、Entryの値は引き続きObjectへの強い参照を維持します。Entry.keyはすでにnullであるため、クライアントには方法がありませんエントリを見つけることができるため、エントリのValueオブジェクトでメモリリークのリスクがあります。ここに画像の説明を挿入

結論:弱い参照にはメモリリークのリスクもあります

PS:
1. ThreadLocalが実際に使用される場合、ThreadLocalへの参照が人為的に破壊されることはほとんどありません。JDKの提案では、静的を使用してThreadLocalを変更することにより、常にThreadLocalへの強い参照を維持します;
2. ThreadLocalMap のget、set、およびremoveメソッドはすべて、Entry.key = nullの場合を考慮します。これらの3つの操作が実行されると、Entry.key = null Entry.value(ソースコードに反映されます)がクリアされ、メモリリークの発生率が大幅に減少します。3。
強い参照と弱い参照に関係なく、メモリリークのリスクがあります。デザインで弱い参照を選択する理由 理由は単純です。弱い参照は強い参照よりもメモリリークの可能性が低いためです。結局、ThreadLocalはメモリ領域を再利用できます!

3.3メモリリークを完全に回避する方法

前の分析は、クライアントがThreadLocalへの参照を切断したことを分析することです。クライアントがThreadオブジェクトThread_refへの強い参照を(割り当てによってThread_ref=null;切断した場合、GCが発生すると、そのThreadオブジェクトは参照されないため、確実に削除され、ThreadLocalMap自体がThreadオブジェクトのプロパティになります。同様に、ThreadLocalMap、ThreadLocal.Entry、およびThreadLocalはGCによって強制終了され、ヒープメモリ全体がクリーンになります。ただし、スレッドは希少なリソースと見なされます。実際のアプリケーションでは、スレッドプールはスレッドをリサイクルするためによく使用されます。したがって、メモリリークを回避するためにクライアントがスレッドオブジェクトの強い参照を切断することは理論的には可能ですが、実際にはそうではありません。

上記の2つのメモリリークのシナリオを思い出してください。ThreadLocalが強く参照されている場合、メモリリークの理由は、Entry.keyがThreadLocalへの強い参照を保持しているため、GCによってThreadLocalをリサイクルできないためです。ThreadLocalが弱く参照されている場合、メモリリークはEntry.keyで発生します。 = nullの場合、Entry値を解放できません。明らかに、どちらの場合も問題はEntryオブジェクトにあります。Entryを直接強制終了した場合、メモリリークは発生しませんか?はい、JDKデザインマスターは当然これを考えていたため、これを行うためにThreadLocal.remove()メソッドを提供しました。

要約すると、メモリリークを回避するためのベストプラクティスは、ThreadLocal.remove()を使用して、ThreadLocalを使用した後にEntryオブジェクト全体をクリアし、次のボイラープレートコードを使用することです。

ThreadLocal.set(value);
try {
   // 这里是业务代码
} finally {
    ThreadLocal.remove();
}

4、ソースコード分析

4.1 ThreadLocalソースコード

最初の画像を比較すると、ThreadLocalを見るのは簡単です。ThreadLocalで一般的に使用されるメソッドは次のとおりです。

  • 初期値()
// 交给给客户端进行覆写自定义初始值的生成
protected T initialValue() {
    return null;
}
  • セット(T値)
public void set(T value) {
    // 获得当前线程
    Thread t = Thread.currentThread();
    // 获得当前线程持有的 ThreadLocal.ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 调用ThreadLocalMap 的set方法;这里的this就是当前触发set方法的ThreadLocal对象本身
        map.set(this, value);
    else
        // 直接 new ThreadLocalMap(this, value) 并赋值给 t.threadLocals
        createMap(t, value);
}


/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    // threadLocals 就是线程持有的 ThreadLocal.ThreadLocalMap 类型变量
    return t.threadLocals;
}
  • T get()
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 调用ThreadLocalMap 的getEntry方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 触发初始化initialValue方法,顺便把set方法的逻辑走一遍,最后返回初始值
    return 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();
    // 下面的内容跟set方法完全一样;这才体现出 setInitialValue 这个方法名的含义!
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    // 返回初始值
    return value;
}
  • 削除する()
 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         // 调用 ThreadLocalMap的remove方法
         m.remove(this);
 }

ThreadLocalだけで4つのメソッドを見ると、実際には非常に明確です。ThreadLocalのファサードは、クラスではなく、あまりにも多くのロジック、重くするために委託実際のロジックとして見ることができThreadLocalMapません。

4.2 ThreadLocalMapソースコード

ThreadLocalMapは実際に機能しています。ThreadLocalの4つのメソッドに対応して、いくつかのメソッドも提供しています:set()-> ThreadLocalMap.set()、get()-> getEntry(ThreadLocal <?> Key)、remove ()->(ThreadLocal <?>キー)を削除すると、ソースコードは次のようになります。

  • ThreadLocalMap.set(ThreadLocal<?> key , Object value)
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);

    // 如果ThreadLocal对应的key找得到,则进行赋值
    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) {
            // Entry值为null, 则进行清理
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 如果ThreadLocal对应的key找不到,则新建Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 顺带做点脏数据清理工作,内部触发 expungeStaleEntry
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

// 清除key=null的Entry,显示将Entry.value=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) {
            // 清除key=null的Entry的value
            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;
}
  • Entry getEntry(ThreadLocal<?> key)
/**
 * 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
        // 什么情况下会进这里来?发生了GC,由于是弱引用,Entry的key指向的ThreadLocal对象已经被GC回收了,但Entry的value还没被清理
        return getEntryAfterMiss(key, i, e);
}

/**
 * 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) {
        // 拿到key指向的ThreadLocal对象
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            // key指向的ThreadLocal对象为null,说明ThreadLocal对象被垃圾回收了,故需要清理掉Entry的value
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
  • remove(ThreadLocal<?> 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();
            // 清除脏数据
            expungeStaleEntry(i);
            return;
        }
    }
}

4.3まとめ

最後に、最後の図で、ThreadLocalの3つのメソッドset、get、およびremoveに関連するオブジェクトと呼び出しリンクを要約します。ここに画像の説明を挿入

結論:

  • ThreadLocalの強参照と弱参照にはすべてメモリリークのリスクがありますが、このリスクは実際にはそれほど大きくありません。ThreadLocalのset、get、およびremoveメソッドはすべて、ダーティデータを削除するための追加の最適化作業を行うためです。
  • ThreadLocalのベストプラクティスは、ThreadLocalを使用した後に積極的にremoveメソッドを実行して、メモリリークを完全に排除することです。

全文〜

元の記事31件を公開 賞賛された32件 40,000回以上の閲覧

おすすめ

転載: blog.csdn.net/caiguoxiong0101/article/details/105355115