注明:本文源码基于JDK1.8版本
記事ディレクトリ
スレッドローカルとは何ですか
ThreadLocal はスレッドローカル変数と呼ばれ、ThreadLocal を使用して変数を保持すると、各 Thread が独自のコピー変数を持ち、複数のスレッドが互いに干渉することがなくなり、スレッド間のデータ分離が実現されます。
ThreadLocal によって維持される変数は、スレッドのライフサイクル内で機能するため、同じスレッド内の複数の関数またはコンポーネント間でパブリック変数を渡す複雑さを軽減できます。
簡単な例から始めましょう。
public class ThreadLocalTest01 {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 子线程t1调用threadLocal的set方法赋值
Thread t1 = new Thread(() ->{
threadLocal.set("abc");
System.out.println("t1 赋值完成");
System.out.println("t1线程中get:"+threadLocal.get());
});
// 子线程t2调用threadLocal的get方法取值
Thread t2 = new Thread(() ->
System.out.println("t2线程中get:"+threadLocal.get())
);
t1.start();
t1.join();
t2.start();
// 主线程调用threadLocal的get方法取值
System.out.println("主线程中get:"+threadLocal.get());
}
}
出力結果:
t1 割り当てが完了しました
スレッド t1 で取得: abc
メインスレッドで取得: null
スレッド t2 で取得: null
プログラムから、set メソッドを介して t1 スレッドの threadLocal に保存されたデータは、他のスレッドからアクセスできないことがわかります。
ThreadLocal データ構造
- 各スレッドは Thread オブジェクトに対応し、Thread オブジェクトには ThreadLocal.ThreadLocalMap メンバー変数があります。
- ThreadLocalMap は、キーと値のペアを保持する HashMap に似ていますが、HashMap のデータ構造が配列 + リンク リスト/赤黒ツリーであるのに対し、ThreadLocalMap のデータ構造は配列である点が異なります。
- ThreadLocalMap 配列には、静的な内部クラス オブジェクト Entry (ThreadLocal<?> k, Object v) が格納されます。単純に、ThreadLocal オブジェクトがキーであり、セットの内容が値であると考えることができます。(実際には、キーは弱い参照 WeakReference<ThreadLocal<?>>)
Java の 4 つの参照型
以前に ThreadLocal データ構造を紹介したときに、ThreadLocalMap のキーは弱参照であると述べました。弱い参照は何に使われるのでしょうか? なぜここで弱い参照を使用するのでしょうか? これらの問題を明確にするために、Java の 4 つの参照型を見てみましょう。
- 強参照: 通常、強参照型を最もよく使用します。たとえば
Object obj = new Object()
、Object オブジェクトを作成すると、スタック メモリに obj 変数が存在し、ヒープ メモリに割り当てられた Object オブジェクトを指します。この種の参照は、は強力な参考資料です。強参照が存在する限り、メモリ不足により JVM が OOM 例外をスローしたとしても、ガベージ コレクターはそれをリサイクルしません。 - ソフト参照: ソフト参照は SoftReference で修飾されます。たとえば
SoftReference<Object> sr = new SoftReference<>(new Object())
、スタック メモリ内に sr があり、これは強参照を通じてヒープ メモリに割り当てられた SoftReference オブジェクトに関連付けられており、SoftReference オブジェクト内にソフト参照が存在します。割り当てられた Object オブジェクトを指します。ソフトリファレンスが指すObjectオブジェクトは、JVMヒープメモリが不足した場合、ガベージコレクタにより回収されます。ソフト参照は、画像キャッシュや Web ページ キャッシュの実装など、いくつかの便利ではあるが必須ではないオブジェクトを記述するために使用されます。 - 弱い参照: 弱い参照は、 などの WeakReference で変更されます
WeakReference<Object> wr = new WeakReference<>(new Object())
。オブジェクトが弱い参照にのみ関連付けられている場合、ガベージ コレクションが発生する限り、オブジェクトはリサイクルされます。弱参照の典型的なアプリケーション シナリオは ThreadLocalMap です。 - ファントム参照: ファントム参照は、PhantomReference で修飾されます。たとえば
PhantomReference<Object> pr = new PhantomReference<>(new Object(), QUEUE)
、ファントム参照は、以前のソフト参照や弱参照とは異なります。オブジェクトのライフサイクルには影響しません。ファントム参照の唯一の機能は、キューを使用して受信することです。オブジェクトがもうすぐ消滅するという通知、この方法でオフヒープ メモリを管理します。Netty のゼロ コピーは、ファントム参照の典型的なアプリケーションです。
ThreadLocalMap のキーが弱い参照を使用するのはなぜですか?
この質問を検討する前に、ThreadLocalMap での弱い参照がどのように見えるかを見てみましょう。
/**
* 测试没有强引用关联ThreadLocal对象时,Entry中的虚引用key是否被回收
*/
public class ThreadLocalTest02_GC {
public static void main(String[] args) throws Exception {
// 有强引用指向ThreadLocal对象
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("abc");
// 没有强引用指向ThreadLocal对象
new ThreadLocal<>().set("def");
// Thread中成员变量threadLocals是默认访问类型,只允许同一个包里类访问,我们可以通过反射方式拿到。
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object threadLocalMap = field.get(t);
Class<?> tlmClass = threadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
}
}
出力は次のとおりです。
キー: java.lang.ThreadLocal@4d7e1886、値: abc
キー: java.lang.ThreadLocal@3cd1a2f1、値: [Ljava.lang.Object;@2f0e140b
キー: java.lang.ThreadLocal@7440e464、値: java.lang. ref.SoftReference@49476842
キー: null、値: def
キー: java.lang.ThreadLocal@78308db1、値: java.lang.ref.SoftReference@27c170f0
出力結果から、値「abc」を持つレコードキーが存在し、値「def」を持つレコードには対応するキーがnullであることがわかります。これは、強参照が存在しない場合、弱参照が指すオブジェクトがガベージ コレクターによって回収されることを意味します。
ThreadLocalMap のキーは弱参照として定義されているため、localThread オブジェクトに強参照ポイントがない場合、メモリ リークを避けるために gc によってリサイクルされます。ただし、ここでのキーはリサイクルされていますが、値には依然としてメモリ リークが発生しています。スレッドのライフサイクルが終了するか、クリーンアップ アルゴリズムがトリガーされた場合にのみ、値を gc によって回収できます。
注:这里除了我们set的两条数据,还有其它三条数据,如StringCoding编解码使用的数据,我们可以忽略
ThreadLocal setメソッドの詳細説明
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("abc");
---------------------------------------------------------
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 从线程对象t中获取ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
---------------------------------------------------------
// 从线程对象t中获取ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
---------------------------------------------------------
// 创建线程对象t的成员变量ThreadLocalMap对象,
// 初始化一条数据:this(指的是threadLocal对象)为key,firstValue为value
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
上記のコードに示すように、情報「abc」を保存するために set メソッドを呼び出すと、まず Thread.currentThread() によって現在のスレッド オブジェクト t を取得し、次にスレッド オブジェクト t 内の ThreadLocalMap 型変数マップを取得します。マップは null ではありません。キー/値のキーと値のペアのデータ (threadLocal がキー、設定値 "abc" が値) を直接挿入します。マップが null の場合は、ThreadLocalMap を作成し、マップが新しく作成したオブジェクトを取得し、キー/値のキーと値のペアのデータ (threadLocal がキー、設定値 "abc" が値) を初期化します。
まず、マップが null の場合にどのような操作が行われるかを見てみましょうnew ThreadLocalMap(this, firstValue)
。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 创建一个Entry数组,数组初始容量为INITIAL_CAPACITY(16)
table = new Entry[INITIAL_CAPACITY];
// 计算下标位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置阈值
setThreshold(INITIAL_CAPACITY);
}
---------------------------------------------------------
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);
}
---------------------------------------------------------
// 设置阈值大小,当数组中的元素大于等于阈值时,会触发rehash方法进行扩容
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
マップが null でない場合を見てみましょう
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 开放定址法查找可用的槽位(用于解决HASH冲突)
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果槽位上已经有值,并且key相同,则替换value值
if (k == key) {
e.value = value;
return;
}
// 如果槽位上有值,并且key已经被GC回收了,触发探测式清理,清理掉过时的条目
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找到空的槽位,将key和value插入此槽位
tab[i] = new Entry(key, value);
int sz = ++size;
// 触发清理,并判断如果清理后的size达到了阈值,则进行rehash进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
---------------------------------------------------------
// 定向寻址,寻找下一个位置,如果到了最后,则再从0下标开始
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
構造的には HashMap とよく似ていますか? Hash 演算によって配列の添え字の位置を見つけて挿入します。違いは、HashMap がハッシュの競合を解決する方法がリンク リスト/赤黒ツリーであるのに対し、ThreadLocalMap はそれを使用することです开放定址法
。(キーがリサイクルされるアイテムをクリーンアップする具体的なアルゴリズム ロジックはここでは紹介しません。興味のある学生はソース コードを参照してください。)
魔法の0x61c88647
値 が表示されます0x61c88647
。ThreadLocal オブジェクトが作成されるたびに、ハッシュコードの増分はこの値になります。これは非常に特別な値です。これは、黄金比とフィボナッチ数列である符号付き整数の 0.618 倍です。ハッシュの増分にこの数値を使用する利点は、ハッシュの分布が非常に均一になることです。
コードを使用して以下を実証します。
public static void main(String[] args) throws IOException {
threadLocalHashTest(16);
System.out.println("-------------------------------------------");
threadLocalHashTest(32);
}
public static void threadLocalHashTest(int n){
int HASH_INCREMENT = 0x61c88647;
int nextHashCode = HASH_INCREMENT;
for(int i=0; i<n; i++ ){
System.out.print((nextHashCode & (n-1)));
System.out.print(" ");
nextHashCode += HASH_INCREMENT;
}
}
出力は次のとおりです。
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23
30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
ThreadLocalMap 拡張メカニズム
ThreadLocalMap.set() メソッドの最後で、クリーニングの実行後に現在のハッシュ配列内のエントリ数がリストの拡張しきい値 (sz >= しきい値) に達した場合、再ハッシュ拡張ロジックが実行されます。rehash 方式でも最初にクリーニング作業を行い、キーが null のエントリをクリアし、クリーニング後、現在のエントリ数がしきい値の 3/4 (サイズ >= しきい値 - しきい値 / 4) に達したかどうかを判断します。到達した場合は、resize メソッドを実行して実容量拡張操作を実行し、容量を 2 倍にしてハッシュ位置を再計算します。
rehash メソッドを実行する必要があるかどうかを判断する場合は、しきい値に達したかどうかを判断基準とし、rehash メソッドを再度実行する必要があるかどうかを rehash 内部で判断する場合は、しきい値の 3/4 に達したかどうかを判断基準とします。これはなぜでしょうか? ソース コードに記載されている説明は次のとおりです: ヒステリシスを避けるために倍加には下限しきい値を使用します (ヒステリシスを避けるには、下限しきい値を 2 倍にして使用します)。
// 如果当前数组中的Entry数量已经大于等于阈值,执行rehash方法
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
---------------------------------------------------------
// 阈值大小规则
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
---------------------------------------------------------
private void rehash() {
// 清理过时条目,也就是key被GC回收掉的条目
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// (使用较低的阈值以避免滞后)
if (size >= threshold - threshold / 4)
resize();
}
---------------------------------------------------------
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
// e.get得到的是key,如果key不存在,则进行清理
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
---------------------------------------------------------
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 新容量扩为之前的2倍
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;
}
ThreadLocal getメソッドの詳細説明
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 从线程对象t中获取ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 通过key(当前ThreadLocal对象)寻找value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果获取不到值,则初始化一个值
return setInitialValue();
}
---------------------------------------------------------
// 通过key(当前ThreadLocal对象)寻找value
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);
}
// 如果hash计算出的位置没有找到,则依据开放定址法去查找
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;
}
---------------------------------------------------------
// map中确实没有,则初始化一个值
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;
}
// ThreadLocal默认初始化返回null,可以自定义具体的返回值
protected T initialValue() {
return null;
}
上記のコードに示すように、get メソッドを呼び出すと、まず Thread.currentThread() を通じて現在のスレッド オブジェクト t を取得し、次にスレッド オブジェクト t 内の ThreadLocalMap 型変数マップを取得します。マップが null でない場合は、現在の threadLocal オブジェクトがキーです クエリでは、クエリはオープン アドレッシング方式の原則に従います。現在のハッシュによって計算された場所が見つからない場合は、後で検索を続けます。それでもマップから所望の結果が見つからない場合は、書き換えられた初期化メソッドに従って値が初期化されます。
初期化のサンプルコードは以下のとおりです。
public class ThreadLocalTest04_init {
public static void main(String[] args) {
ThreadLocal<String> tl = new ThreadLocal(){
@Override
protected String initialValue(){
return "default value";
}
};
System.out.println(tl.get());
}
}
出力は次のとおりです。
デフォルト値
継承可能なスレッドローカル
InheritableThreadLocal の簡単な紹介
ThreadLocalを使用すると、親スレッドが保存したデータをsetメソッドで子スレッドが取得できないため、子スレッドにも取得させたい場合は、InheritableThreadLocalクラスを使用します。
public class ThreadLocalTest03 {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
threadLocal.set("threadLocal value");
inheritableThreadLocal.set("inheritableThreadLocal value");
new Thread(() -> {
System.out.println("子线程获取父线程threadLocal数据:" + threadLocal.get());
System.out.println("子线程获取父线程inheritableThreadLocal数据:" + inheritableThreadLocal.get());
}).start();
}
}
出力は次のとおりです。
子スレッドは親スレッドの threadLocal データを取得します。null
子スレッドは親スレッドの継承可能なスレッドローカル データを取得します。
この例では、InheritableThreadLocal を使用して親スレッドに設定されたコンテンツが、子スレッドの get メソッドによって取得できることがわかります。
InheritableThreadLocal の原則
では、子スレッドが親スレッドによって保存されたコンテンツを取得できることを実現するにはどうすればよいでしょうか? 原理を分析してみましょう。
まず、スレッド クラス Thread には、ThreadLocalMap メンバー変数が 2 つあり、1 つは共通の ThreadLocal 関連情報を格納するために使用され、もう 1 つは InheritableThreadLocal 関連情報を格納するために使用されます。
// 用来保存ThreadLocal相关信息
ThreadLocal.ThreadLocalMap threadLocals = null;
// 用来保存InheritableThreadLocal相关信息
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
InheritableThreadLocal オブジェクトが情報を保存するために set メソッドを呼び出すと、次のように親 ThreadLocal オブジェクトの set メソッドが呼び出されます。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
このうち、getMap メソッドと createMap メソッドは、InheritableThreadLocal オブジェクトによって書き換えられています。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
これを見ると、 Thread スレッドオブジェクトのメンバ変数に格納されている InheritableThreadLocal の情報が分かるのですが、ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
明確に説明されていないのですが、サブスレッドはどうやってそれを取得するのでしょうか?
このキーポイントはnew Thread()
次のとおりです。
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
---------------------------------------------------------
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null);
}
---------------------------------------------------------
// 为了直观,这里我用省略号代替了其它的一些逻辑
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
......
Thread parent = currentThread();
......
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
......
}
ここで、子スレッドを作成する場合は、現在のスレッド オブジェクト (つまり、親スレッド オブジェクト) を取得し、現在のスレッド オブジェクトのメンバー変数 inheritableThreadLocals のデータを、作成される子スレッドにコピーします。したがって、子スレッドのコンテンツを取得できます。
InheritableThreadLocal に関する注意事項
- 一般的に非同期処理にはスレッドプールを使用しますが、InheritableThreadLocalはnew Threadのinit()メソッドで割り当てられており、スレッドプールはスレッド再利用のロジックであるため、ここで問題が発生します。
- スレッド プールなどのスレッドをキャッシュするコンポーネントを使用するときに ThreadLocal を子スレッドに渡すには、Alibaba オープン ソース コンポーネントを使用できます
TransmittableThreadLocal
。 - 子スレッドのデータは親スレッドからコピーされるため、子スレッドでリセットされたコンテンツは親スレッドには表示されません。
ThreadLocal アプリケーションのケース
データベース接続を管理します。
クラスAのメソッドaがクラスBのメソッドbとクラスCのメソッドcを呼び出すと、メソッドaがトランザクションを開始し、メソッドbとメソッドcがデータベースを操作します。トランザクションを実装するには、メソッド b とメソッド c で使用されるデータベース接続が同じ接続である必要があることはわかっていますが、同じデータベース接続が使用されていることをどのように認識できるでしょうか。答えは、ThreadLocal を通じて管理することです。
MDC ログ リンクの追跡。
MDC (Mapped Diagnostic Contexts) は、主に各リクエストのコンテキスト パラメーターを保存するために使用されますが、同時に %X{key} をログ出力形式で直接使用して、コンテキスト内のパラメーターをログの各行に出力することもできます。コンテキスト情報の保存は、主に ThreadLocal を通じて実現されます。
トランザクション プロセスの各リンクのログにグローバル シリアル番号 transId を出力する場合、プロセスには複数のシステム、複数のスレッド、および複数のメソッドが関与する可能性があります。一部のリンクでは、グローバル シリアル番号をパラメーターとして渡すことができないため、transId パラメーターを取得するにはどうすればよいですか? ここでは、Threadlocal 機能を使用します。各システムやスレッドはリクエストを受信すると、transIdをThreadLocalに格納し、ログ出力時にtransIdを取得して出力します。このようにして、transId を介してログ ファイル内のリンク全体のログ情報をクエリできます。
ThreadLocal を使用する場合の注意事項
メモリリークまたはダーティデータ。スレッドを使用する場合、ほとんどの場合はスレッド プールで管理されるため、使用後に一部のスレッドが破棄されることはありませんが、ThreadLocal が Remove メソッドを実行しないと、保存されたデータが常に存在し、メモリ リークが発生します。この時点で ThreadLocal オブジェクトも静的定数である場合、次回スレッドが使用されるときに、以前に保存されたデータが取得される可能性があり、結果としてデータがダーティになります。したがって、ThreadLocal を使用する場合は、必ず最後に Remove メソッドを呼び出してください。