【ソースコード】スレッドローカルソースコード超詳しい解説

スレッドローカルとは

Threadlocal は、スレッド自体のみがアクセスできるスレッド自体のローカル変数として理解でき、各スレッドは独自のスレッドローカルを維持します。

使い方

使い方はとてもシンプルで、核となるのはset/getの2つのメソッド

public class TestThreadLocal {
    
    

    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
    
    
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    threadLocal.set("aaa");
                    Thread.sleep(500);
                    System.out.println("threadA:" + threadLocal.get());
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                threadLocal.set("bbb");
                System.out.println("threadB:" + threadLocal.get());
            }
        }).start();
    }

}

運用実績

threadB:bbb
threadA:aaa

上記のコードから、2 つのスレッドがスレッドローカルを使用していることがわかります.最初のスレッドは 2 番目のスレッドの前に値を設定し、2 番目のスレッドは最初のスレッドの前に値を取得します.取得された値は影響を受けないことがわかります.各スレッドはこのスレッドの変数のみを取得できます。

適用シナリオ

どのようなシーンで使用できますか?

  1. In a multi-tenant system, you can parse the tenant id in the global pre-interceptor and put it in threadlocal and encapsulate a util. ​​コントローラー/サービスは、解析を繰り返すことなく TenementUtil.get() から直接取得できます。メソッドはテナント ID を再度解析する必要があるため、多くのコードの冗長性が生じます
  2. 同じロジックで、ユーザーの情報分析をグローバル プリインターセプターのスレッドローカルに入れることができます。
  3. 私のxxl-job ソース コードの記事を読んだことがあれば、xxl-job エグゼキューターによってカプセル化されたコンテキスト オブジェクトもスレッドローカルに保存されることを知っているはずです。

ThreadLocal はどのようにしてスレッドに複数の ThreadLocal オブジェクトを持たせるのですか?

変数は各クラスThreadで維持されますthreadLocals

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMapコンストラクタを見てみましょう

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    
    
    // 初始化entry表
    table = new Entry[INITIAL_CAPACITY];
    // 计算table表的位置
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    // 当前size
    size = 1;
    // 设置阈值
    setThreshold(INITIAL_CAPACITY);
}

threadLocalHashCodethreadlocalmap がテーブル配列を維持し、配列の場所が ThreadLocal によって保証されていることがわかります。

threadLocalHashCode のロジックを見て、ThreadLocalこのコードを見つけてみましょう

// 本对象私有常量
private final int threadLocalHashCode = nextHashCode();
// 共享原子对象
private static AtomicInteger nextHashCode =
    new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    
    
  	// 获取并增加,原子操作
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

各 ThreadLocal オブジェクトが最終的な threadLocalHashCode を保持し、静的メソッド AtomicInteger.getAndAdd を通じて定数が取得されることがわかります.AtomicInteger は cas に基づくアトミック操作であるため、同時作成では重複はありません.この場合、各 ThreadLocal オブジェクトの threadLocalHashCode は、完全に一意であることが保証されます。つまり、新しい ThreadLocal ごとに異なる threadLocalHashCode 値があります。

ThreadLocal メモリ リークの問題

メモリリークに関しては、まずストレージ構造とスレッドローカルの参照を理解する必要があります.最初にThreadLocalMap構築方法を見てみましょう.

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    
    
    // 初始化entry表
    table = new Entry[INITIAL_CAPACITY];
    // 计算table表的位置
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    // 当前size
    size = 1;
    // 设置阈值
    setThreshold(INITIAL_CAPACITY);
}

ここでオブジェクトがストレージに使用されていることがわかりますEntry。その構造を見てみましょう

static class Entry extends WeakReference<ThreadLocal<?>> {
    
    
    
    Object value;

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

WeakReferenceEntry オブジェクトが(弱参照) オブジェクトを継承していることがわかりますが、なぜここで弱参照を使用するのでしょうか?

ThreadLocal 参照構造は下の図で見ることができます. 点線が弱参照, 実線が強参照. 強参照と弱参照が分からない方は前回の記事を読んでください. . JAVA の参照型、強参照、軟参照、弱参照、仮想参照
ここに画像の説明を挿入

ThreadLocal が Entry のキーであり、弱参照であることがわかります.ThreadLocal オブジェクトへの外部の強参照がない場合、ThreadLocal は次回の gc リサイクル時にリサイクルされます。

使用時には、メンバ変数の位置に ThreadLocal を配置し、静的な装飾を使用して ThreadLocal インスタンスを頻繁に作成しないようにすることをお勧めします。

大きなオブジェクトを保存するときは注意してください. 必要がない場合は, 大きなオブジェクトを保存しないようにしてください. どうしても必要な場合は, ヒープメモリのオーバーフローを避けるために, 使用後にオブジェクトを削除するメソッドを呼び出すことをお勧めしますremove.

スレッドが終了すると、ThreadLocalMap および Entry テーブルがリサイクルされます。

set(T値)メソッド

現れるjava.lang.ThreadLocal#set

public void set(T value) {
    
    
    // 获得当前线程实例
    Thread t = Thread.currentThread();
    // 获得存储对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    
    
        // 设置value
        map.set(this, value);
    } else {
    
    
        // 创建并设置
        createMap(t, value);
    }
}

設定すると、現在のオブジェクトを取得し、Thread オブジェクトから threadLocalMap オブジェクトを取得しようとします。そうでない場合は作成します。

getMap方法を見てみcreateMapましょう

ThreadLocalMap getMap(Thread t) {
    
    
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    
    
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

これは、各スレッドが独立した threadLocalMap オブジェクトを持っていることを意味し、Thread の threadLocals オブジェクトが空の場合、ThreadLocalMap オブジェクトが作成されます。

次に、値の設定方法を見てみましょうjava.lang.ThreadLocal.ThreadLocalMap#set

private void set(ThreadLocal<?> key, Object value) {
    
    
    Entry[] tab = table;
    int len = tab.length;
    // 计算数组下标
    int i = key.threadLocalHashCode & (len-1);

    // 找到相同的threadlocal,如果找不索引加一继续找
    // 此处跳出有两个条件,找到相同的threadlocal,或者找k为null
    // 此处主要作用就是看数组中是否存在threadlocal,存在就覆盖赋值
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
    
    
        ThreadLocal<?> k = e.get();

        if (k == key) {
    
    
            // 如果找到threadlocal就直接修改值并结束set
            e.value = value;
            return;
        }

        if (k == null) {
    
    
            // 不存在需要替换过期值
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 代码走到这里说明没有找到threadlocal,也不存在过期值,那么直接给索引位置设置entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 扩容动作
        rehash();
}

ここに注意してくださいreplaceStaleEntry.期限切れの値があることを示すためにコードがここに行きます.先ほどスレッドローカルはエントリによって弱参照されると言いました.メインスレッドに強い参照がない場合,それは​​当然gcによってリサイクルされます.このとき. 、エントリ オブジェクトが存在するが、キー値が存在しない状況がある場合、このメソッドが呼び出されてデータが置き換えられます。

採用开放地址法、添字が競合する場合、次の空の位置を探しに行く

上記のコードを次の図で説明しましょう. entry1-5 があるとします. 今, entry1, entry2, entry3, entry4 がテーブルに格納されています. 今, entry5 を格納したいのですが, 計算された添え字は 2 です. 最終的に, entry5実際には添字 3 に格納されます

画像-20220416105550366

画像-20220416105816260

entry3 (つまり、threadlocal) のキーがその強い参照を失った後も、そのエントリは引き続きテーブル配列に格納され、計算されたインデックスがその場所にあるときに置き換えられます。

画像-20220416110058336

交換コードを見てみましょうjava.lang.ThreadLocal.ThreadLocalMap#replaceStaleEntry

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    
    
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    int slotToExpunge = staleSlot;
    // 向前找到最小失效的下标
    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();

        // 找到相同的key则需要进行置换动作
        if (k == key) {
    
    
            // 置换value 和key
            e.value = value;

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

            // 如果第一层循环没有找到任何对象,需要将数据对其
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 清理过期entry 结束置换动作
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        // 如果key不存在,且前面不存在失效的key,则将数据对其
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 没有找到相同的key则直接new一个新的entry放进去
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 清楚其他的过期对象
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

期限切れの状況が発生した場合、直接クリアされるのではなく、以前のエントリ オブジェクトが再利用され、エントリが存在しない場合、新しいエントリが作成されることがわかります。

get() メソッド

ここまでの内容でスレッドローカル全体の動作原理と参照構造を理解できたので、次にgetメソッドを深く分析していきます。

java.lang.ThreadLocal#get

public T get() {
    
    
    // 获得当前线程
    Thread t = Thread.currentThread();
    // 获得threadlocalmap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    
    
        // 获得entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
    
    
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            // 正常返回
            return result;
        }
    }
    // 当threadlocalmap不存在时 初始化threadlocalmap并返回
    return setInitialValue();
}

java.lang.ThreadLocal.ThreadLocalMap#getEntry

private Entry getEntry(ThreadLocal<?> key) {
    
    
    // 计算entry table索引
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        // 当entry的值不存在时
        return getEntryAfterMiss(key, i, e);
}

java.lang.ThreadLocal.ThreadLocalMap#getEntryAfterMiss

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)
            // 找到entry
            return e;
        if (k == null)
            // 移除过期条目
            expungeStaleEntry(i);
        else
            // 向下扫描
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

ハッシュマップを直接使用しない理由

通常、ThreadLocal に格納されるデータの量はそれほど大きくありません. 削除された後、ガベージ コレクタによって再利用されます. データ構造は単なる配列です. この格納方法を使用すると、スペースが節約され、配列添字のクエリ効率が向上しますも高いです。

データ構造:
hashmap は配列 + 連結リスト + 赤黒木
threadlocalmap は配列のみ

参照タイプ:
hashmap 値は強い参照であり、メモリ解放を助長しません
.threadlocalmap は弱い参照であり、メモリ パフォーマンスが優れています

ハッシュ競合:
hashmap は、リンクされたリスト/赤黒ツリーによって競合を解決します
threadlocalmap は、オープン アドレス指定によって競合を解決します

パフォーマンスに関しては、
hashmap は大量のデータでより優れたパフォーマンスを発揮し、
threadlocalmap は少量のデータでより優れたパフォーマンスを発揮します。

おすすめ

転載: blog.csdn.net/qq_21046665/article/details/124211071