ThreadLocal使用上の注意:スレッドは安全ではなく、メモリリークが発生する可能性があります



Threadlocal Use Note



メモリリークが発生する可能性があるとしましょう。

序文

ThreadLocal役割は、スレッド内でローカル変数を提供することです。これは、スレッドの存続期間中に機能し、同じスレッド内の複数の関数またはコンポーネント間でのいくつかのパブリック変数の転送の複雑さを軽減します。ただし、悪用されるとThreadLocal、メモリリークが発生する可能性があります。以下では、3つの側面について分析しますThreadLocalメモリリークの問題



  • ThreadLocal実装の原則
  • ThreadLocalメモリリークが発生するのはなぜですか?
  • ThreadLocalベストプラクティス

ThreadLocal実装の原則

ThreadLocal



ThreadLocal実装は次のようになります:各Thread 1つを維持ThreadLocalMapマッピングテーブル、このマッピングテーブルkeyはいThreadLocalインスタンス自体value本当に保存する必要がありますObject

つまり、ThreadLocal値自体を格納せず、a keyスレッドをThreadLocalMapから解放するために機能します。 valueを取得します。図の点線は、ThreadLocalMap使用するThreadLocal弱参照Key弱参照オブジェクトがGCでリサイクルされることを示していることに注意してください。

ThreadLocalメモリリークが発生するのはなぜですか?

ThreadLocalMap使用ThreadLocal弱い参照key参照する外部の強い参照がない場合|システムGCの場合、これThreadLocalバインドされますリサイクルされるため、ThreadLocalに表示されますThreadLocalMap for key of nullこれらにアクセスする方法はありませんEntry for key of null of Entry現在のスレッドが遅れて終了しない場合、これらvalue for key of null of Entry常に強い参照のチェーン:valueリサイクルできないため、メモリリークが発生します。



実際、Thread Ref -> Thread -> ThreaLocalMap -> Entry -> valueこの状況は設計で考慮されており、いくつかの保護対策が加えられています。ThreadLocalMap of ThreadLocalget()set()スレッドremove()オールインThreadLocalMap for key of null

ただし、これらの受動的な予防措置は、メモリリークが発生しないことを保証するものではありません。

  • 使用value of static拡張ThreadLocalライフサイクル、メモリリークの可能性(参照 ThreadLocalメモリリークのインスタンス分析 )。
  • 使用するように割り当てられていますThreadLocal呼び出されなくなりましたThreadLocalget()set()メソッドを使用すると、メモリリークが発生します。

弱参照を使用する理由

表面的には、メモリリークの根本的な原因は、弱参照の使用です。オンライン記事は主に分析に焦点を当てていますremove()弱い参照を使用するとメモリリークが発生する可能性がありますが、別の質問も同様に検討する価値があります。強い参照の代わりに弱い参照を使用するのはなぜですか。

まず、公式ドキュメントを見てみましょう。

非常に大規模で長期的な使用に対処するために、ハッシュテーブルエントリはキーにWeakReferencesを使用します。
非常に大規模で長期的な使用を処理するために、ハッシュテーブルは弱く参照されるキーを使用します。

以下では、2つのケースについて説明します。

  • キーは強力な参照を使用します :引用ThreadLocalオブジェクトはリサイクルされますが、ThreadLocal保持中ThreadLocalMap手動で削除しない場合、強力な参照ThreadLocalリサイクルされないため、結果はThreadLocalメモリーリーク。
  • キーは弱参照を使用します :引用Entryオブジェクトがリサイクルされた理由ThreadLocal hold ThreadLocalMap手動で削除しなくても参照が弱いThreadLocalまたリサイクルされます。ThreadLocal次回value転送ThreadLocalMapsetgetクリアされます。

2つの状況を比較すると、次のことがわかります。removeライフサイクルThreadLocalMap手動で削除しない限りThreadメモリリークが発生しますが、弱参照を使用すると追加の保護層を提供します。 弱い参照keyメモリリークなし、対応ThreadLocal次回value転送ThreadLocalMapsetgetクリアされます

したがって、removeメモリリークの根本的な原因は次のとおりです。ThreadLocalライフサイクルThreadLocalMap手動で削除しない限りThreadこれは、弱い参照が原因ではなく、メモリリークにつながる可能性があります。

ThreadLocalのベストプラクティス

上記の分析を組み合わせると、keyメモリリークの原因と影響を理解できます。次に、メモリリークを回避する方法を教えてください。

  • 使用するたびにThreadLocal、all ThreadLocalメソッドを呼び出してデータをクリアします。

スレッドプールを使用する場合、時間内にクリーンアップされませんでしたremove()メモリリークの問題であるだけでなく、より深刻な場合、ビジネスロジックで問題が発生する可能性があります。したがって、ThreadLocalロックを解除した後にロックを解除するのと同じように、完了したらクリーンアップします。

スレッドは安全ではありません:

ご存知のように、ThreadLocalクラスは、スレッドのセキュリティを実現するのに役立ちます。このクラスは、スレッド内の値を、値を保持するオブジェクトに関連付けることができます。 ThreadLocalは、getやsetなどのアクセスインターフェイスまたはメソッドを提供します。これらのメソッドには、変数を使用するスレッドごとに個別のコピーがあるため、getは、セットが呼び出されたときに、現在のスレッドによって設定された最新の値を常に返します。概念的には、ThreadLocalをMapを含むオブジェクトとして理解します。ここで、Mapのキーはさまざまなスレッドを識別するために使用され、Mapの値はスレッドの特定の値を格納します。しかし、ThreadLocalの実装はそうではありません。このようにThreadLocalを使用しても、真のスレッドセーフを実現することはできません。

Numberがintメンバー変数を持つクラスであることを説明する例を見てみましょう。

画像

ThreadLocal

画像

NotSafeThreadは、Runableインターフェイスを実装するクラスです。さまざまなスレッドのnum値を格納するために、タイプThreadLocalの変数値を作成します。次に、スレッドプールを使用して5つのスレッドを開始します。 ThreadLocalを使用したいと思います。このクラスは、5つの異なるスレッドのNumberタイプのコピーを格納し、変数の共有を根絶し、ThreadLocalクラスのget()メソッドを呼び出すときにスレッドに関連付けられたNumberオブジェクトを返します。あなた自身の数を追跡します:

画像

public class Number { private int num public int getNum() { return num } public void setNum(int num) { this.num = num } @Override public String toString() { return 'Number [num=' + num + ']' } }

画像

プログラムを開始します:結果を出力します

public class NotSafeThread implements Runnable { public static Number number = new Number() public static int i = 0 public void run() { / / Each thread count plus one number.setNum(i++) / / Store it in ThreadLocal value.set(number) / / Output num value System.out.println(value.get().getNum()) } public static ThreadLocal value = new ThreadLocal() { } public static void main(String[] args) { ExecutorService newCachedThreadPool = Executors.newCachedThreadPool() for (int i = 0 i <5 i++) { newCachedThreadPool.execute(new NotSafeThread()) } } }

すべてが正常であるように見えます。各スレッドにはNumber用の独自のストレージスペースがあるようですが、出力の前に遅延を追加するだけです。

画像

0 1 2 3 4

画像

プログラムを実行し、出力します。

public class NotSafeThread implements Runnable { public static Number number = new Number() public static int i = 0 public void run() { / / Each thread count plus one number.setNum(i++) / / Store it in ThreadLocal value.set(number) //delay 2 seconds try { TimeUnit.SECONDS.sleep(2) } catch (InterruptedException e) { // TODO Auto-generated catch block } / / Output num value System.out.println(value.get().getNum()) } public static ThreadLocal value = new ThreadLocal() { } public static void main(String[] args) { ExecutorService newCachedThreadPool = Executors.newCachedThreadPool() for (int i = 0 i <5 i++) { newCachedThreadPool.execute(new NotSafeThread()) } } }

各スレッドが4を出力するのはなぜですか?彼らは自分のNumberのコピーだけを保存しませんか?他のスレッドがまだこの値を変更できるのはなぜですか? ThreadLocalのソースコードを見てみましょう。

画像

4 4 4 4 4

画像

getMapメソッドの場所:

 public void set(Object obj) { Thread thread = Thread.currentThread()//Get the current thread ThreadLocalMap threadlocalmap = getMap(thread) if(threadlocalmap != null) threadlocalmap.set(this, obj) else createMap(thread, obj) }

ご覧のとおり、これらのスレッド固有の値は、ThreadLocalオブジェクトではなく、現在のThreadオブジェクトに格納されています。また、ThreadオブジェクトはObjectオブジェクトへの参照を保持しているため、他のスレッドが参照が指すオブジェクトに変更を加えると、現在のスレッドThreadオブジェクトに保存されている値も変更されることがわかりました。これが、上記のプログラムが同じ結果を出力する理由です。5つのスレッドが同じNumberオブジェクトの参照を保存します。スレッドが2秒間スリープすると、他のスレッドがnum変数を変更するため、最終的な出力結果は同じになります。

次に、ThreadLocal 'は、変数を使用するスレッドごとに個別のコピーを持っているため、getは、セットが呼び出されたときに、現在のスレッドによって設定された最新の値を常に返します。この文の「独立したコピー」つまり、「スレッドローカルストレージ」は各スレッドに固有のオブジェクトであり、他のスレッドと共有されないことを理解しています。これはおそらく当てはまります:

 ThreadLocal.ThreadLocalMap getMap(Thread thread) { Return thread.inheritableThreadLocals / / returns the member variable of thread }

または

 public static ThreadLocal value = new ThreadLocal() { Public Number initialValue(){// Initializes the value held by each thread return new Number() } }

わかりました...現時点では、次のように言うと推定されます。このThreadLocalの用途は何ですか?各スレッドは独自の新しいオブジェクトを使用します。共有せずにこのオブジェクトを使用する場合にのみ、プログラムはスレッドセーフである必要があります。これは私がThreadLocalを使用していないようです。オブジェクトを使用する必要がある場合、このスレッドに新しいオブジェクトを直接使用するのはよくありません。

確かに、ThreadLocalの使用は、複数のスレッドがオブジェクトを一緒に使用できるようにすることではありませんが、オブジェクトoを使用する必要があるスレッドAがあり、このオブジェクトoはこのスレッドA内の複数の場所で呼び出されます。このオブジェクトoを複数のメソッド間でパラメーターとして渡したくないので、このオブジェクトoをTheadLocalに入れて、スレッドA内にある限り、このスレッドA内の任意の場所に配置します。メソッドはこのオブジェクトoを変更しません。同じ変数oを取得できます。

実際に適用される別の例を見てみましょう。たとえば、銀行用のBankDAOクラスと、個人アカウント用のPeopleDAOクラスがあります。今、私たちは銀行に送金する個人が必要です。 PeopleDAOクラスのアカウントを減らす方法があります。 bankDAOクラスがあります。アカウント増加メソッド、次にこれら2つのメソッドは呼び出すときに同じConnectionデータベース接続オブジェクトを使用する必要があります。2つのConnectionオブジェクトを使用すると、トランザクションの2つの段落が開き、個人アカウントが減少し、銀行口座現象が増加しない可能性があります。 。同じConnectionオブジェクトを使用して、各メソッドを呼び出すときにConnectionオブジェクトを渡さないように、アプリケーションでグローバルデータベース接続オブジェクトとして設定できます。問題は、Connectionオブジェクトをグローバル変数として設定した場合、他のスレッドがConnectionオブジェクトを閉じることを保証できないため、スレッドセーフの問題が発生する可能性があることです。解決策は、ThreadLocalを使用して、転送操作のスレッドでConnectionオブジェクトを取得することです。このように、個人口座の削減と銀行口座の増加を呼び出すスレッドでは、同じConnectionオブジェクトをThreadLocalから取得でき、Connectionオブジェクトはこのスレッドに固有であり、他のスレッドの影響を受けません。スレッド、スレッドの安全性を確保します。

コードは次のように表示されます。

画像

 public void run() { value.set(new Number()) }

画像

フレームワークでは、トランザクションコンテキスト(トランザクションコンテキスト)を実行中のスレッドに関連付ける必要があります。トランザクションコンテキストを静的ThreaLocalオブジェクトに保存することで(このコンテキストは他のスレッドとは絶対に共有されません)、この機能を簡単に実装できます。フレームワークコードが現在実行中のトランザクションを判別する必要がある場合、これからトランザクションコンテキストが読み取られます。 ThreadLocalオブジェクトで、各メソッドを呼び出すときに実行コンテキスト情報を渡す必要がないようにします。