ThreadLocal を 1 つの記事で理解したら、インタビュアーに質問してみましょう

1. ThreadLocalの概要

ThreadLocal の役割と使用法

ThreadLocalデータを各スレッドに関連付けるメカニズムを提供する Java のスレッド レベルの変数です。各スレッドには独自の独立したThreadLocalインスタンスがあり、他のスレッドのデータと競合することなく、このインスタンスにデータを保存および取得できます。

ThreadLocal機能と用途には主に次のような側面が含まれます。

  1. スレッドプライベートデータの保存:ThreadLocal各スレッドに必要なプライベートデータを保存するために使用できます。たとえば、マルチスレッド環境で、スレッド間で共有する必要があるオブジェクトがあり、各スレッドにそのプライベート コピーを持たせたい場合は、 を使用してオブジェクトを保存できますThreadLocalこのようにして、各スレッドは、相互に干渉することなく、独自のプライベート コピーを独立して読み取り、変更できます。

  2. パフォーマンスの向上:ThreadLocal共有データを保護するためにスレッド同期メカニズム (ロックなど) の使用を回避できるため、プログラムの同時パフォーマンスが向上します。各スレッドにはデータの独自のコピーがあるため、スレッド間で競合や競合が発生せず、ロックの競合によるパフォーマンスの低下が回避されます。

  3. スレッド固有のリソースを管理する: シナリオによっては、各スレッドに特定のリソースを割り当て、スレッドの終了時にクリーンアップする必要があります。ThreadLocalスレッド固有のリソースをオブジェクトに格納して管理することにより、これらのリソースをスレッドに簡単に関連付けることができ、スレッドの終了時に自動的にクリーンアップされます。

  4. コンテキスト切り替えの問題を解決する: データベース接続、セッション管理など、コンテキスト関係を維持する必要がある一部のシナリオでは、 を使用することでThreadLocalコンテキスト切り替えの問題を十分に解決できます。コンテキスト依存情報を に保存するとThreadLocal、パラメータの受け渡しやグローバル変数へのアクセスを通じて情報を維持することなく、同じスレッド内で情報を共有できます。

要約すると、ThreadLocal各スレッドがそのスコープ内でデータを保存およびアクセスできるようにするシンプルかつ効果的な方法を提供し、それによってスレッドレベルのデータ分離とスレッドの安全性を実現します。マルチスレッド プログラミングで広く使用されており、一般的なアプリケーション シナリオには、スレッド プール、Web アプリケーションのセッション管理、データベース接続管理などが含まれます。ただし、過度の使用によるメモリ リークやリソースの浪費などの問題ThreadLocalを避けるために、使用する際には注意が必要ですThreadLocal

ThreadLocal の原理と実装

ThreadLocal原理と実装方法には、スレッド間のデータ分離とスレッド専用の記憶領域が含まれます。

  1. ThreadLocal原理:

    • 各スレッドには独自のThreadLocalインスタンスがあり、内部でThreadLocalMapオブジェクトを維持します。
    • ThreadLocalMapスレッド ローカル変数の値を格納するために使用されるハッシュ テーブル (ハッシュ テーブル) です。各要素はキーと値のペアであり、キーはインスタンス、値は対応するスレッドのローカル変数ですThreadLocal
    • ThreadLocalを通じて値を取得または設定する場合は、まずThreadLocalMap現在のスレッドに従って対応するオブジェクトを取得し、次にThreadLocalインスタンスをキーとして使用して、対応する値を見つけます。
    • 各スレッドは独立して自身のデータを保持し、異なるスレッド間のデータは互いに干渉しないため、スレッド間のデータの分離が実現されます。
  2. ThreadLocal実現方法:

    • ThreadLocalメモリ リークを防ぐには、弱い参照 (WeakReference) を使用します。ThreadLocalインスタンス自体は強参照ですが、各スレッドに関連付けられたローカル変数は弱参照です。スレッドがリサイクルされると、対応するローカル変数も自動的にリサイクルされます。
    • ThreadLocalのメソッドが呼び出されるset()、実際には受信値を現在のスレッドに関連付け、現在のスレッドの に格納しますThreadLocalMap
    • ThreadLocalのメソッドが呼び出されるget()、実際には現在のスレッドのインスタンスから対応する値を検索して返しますThreadLocalMap見つからない場合は、指定されたデフォルト値をThreadLocal返します。null
    • マルチスレッド環境では、各スレッドが独自の独立した を持っているためThreadLocalMap、各スレッドは他のスレッドのデータに影響を与えることなく、独自のローカル変数を独立して読み取り、変更できます。

ThreadLocalの設計目標は、通信メカニズムとしてではなく、スレッドレベルのデータ分離を提供することであることに注意してください。したがって、ThreadLocalを使用する場合は乱用を避け、発生する可能性のあるリソース リーク、不正なデータ共有、およびメモリ使用量の問題に適切に対処する必要があります。

要約すると、ThreadLocal各スレッドを独立して使用して、ThreadLocalMapスレッドレベルのデータ分離を実現します。弱い参照を使用してメモリ リークを回避し、各スレッドがそのスコープ内のデータを保存およびアクセスするためのシンプルなインターフェイスを提供します。このメカニズムはマルチスレッド プログラミングで非常に役立ち、同時実行パフォーマンスを向上させ、プログラミング モデルを簡素化できます。

マルチスレッド環境における ThreadLocal のアプリケーション シナリオ

  1. スレッド プール: スレッド プールでは、複数のスレッドが 1ThreadLocalつのインスタンスを共有しますが、各スレッドは独自のローカル変数を個別に読み取り、変更できます。これは、スレッドの安全性とデータの分離を維持しながら、スレッド間でデータを共有する必要がある場合に役立ちます。

  2. ThreadLocalWeb アプリケーションのセッション管理: Web アプリケーションでは、ユーザー認証情報、リクエスト コンテキストなどの各ユーザーのセッション情報を保存するために使用できます。を介してThreadLocal、パラメータを明示的に渡すことなく、複数のメソッド呼び出し間でこの情報を共有できるため、アクセスと管理に便利です。

  3. データベース接続管理: マルチスレッド環境でデータベース接続を使用する場合、各スレッドは独立したデータベース接続を持ち、スレッド間のデータが相互に干渉しないようにする必要があります。を使用してThreadLocal各スレッドのデータベース接続を管理すると、各スレッドが独自の接続を取得できるようになり、スレッド間の競合や同期の問題を回避できます。

  4. 日時の書式設定: マルチスレッド環境では、日時の書式設定はスレッドセーフではない操作です。を使用するとThreadLocal、各スレッドに独立した日時フォーマッタを提供できるため、スレッドの安全性の問題が回避され、パフォーマンスが向上します。

  5. ロギング: ロギングは、マルチスレッド アプリケーションでは非常に一般的な要件です。ThreadLocalスレッドごとのロガー インスタンスは、各スレッドが独自のロギング コンテキストを持ち、相互に干渉しないようにするために、 を使用して保存できます。

  6. ユーザー コンテキストの管理: 一部のアプリケーションでは、ユーザー コンテキストに簡単にアクセスして複数のメソッドまたはモジュールで使用できるように、ユーザー情報を現在のスレッドにバインドする必要があります。この要件は、各スレッドが独自の独立したユーザー コンテキストを持つようにすることでThreadLocal簡単に実現できます。

2、ThreadLocal を使用する

  1. 次の型の変数を宣言しますThreadLocal

    private static ThreadLocal<T> threadLocal = new ThreadLocal<>();
    

    ここで、Tは に格納されているThreadLocal値の型です。

  2. ThreadLocalクラスのメソッドを使用してset()値を設定します。

    threadLocal.set(value);
    

    これは、value現在のスレッドの のThreadLocalインスタンスに保存されます。

  3. ThreadLocalクラスのメソッドを使用してget()値を取得します。

    T value = threadLocal.get();
    

    これにより、現在のスレッドのインスタンスThreadLocalに格納されている値が返されます。

  4. ThreadLocalクラスのメソッドを使用してremove()値をクリアします (オプション)。

    threadLocal.remove();
    

    これにより、ThreadLocal現在のスレッドのインスタンスから値が削除されます。

  5. 最後に、オブジェクトが不要になったときにリソースをクリーンアップするためにメソッドThreadLocalを呼び出す必要があります。remove()

    threadLocal.remove();
    

    これにより、潜在的なメモリ リークの問題が回避されます。

およびThreadLocalメソッドはすべて現在のスレッドに対する操作であることに注意してください。したがって、 を使用する場合は、同じスレッド スコープ内で同じオブジェクトを使用するようにしてくださいこの方法によってのみ、同じスレッド内の複数のメソッドまたはコード セグメント間で同じインスタンスが共有されるようにすることができます。set()get()ThreadLocalThreadLocalThreadLocal

さらに、ThreadLocalに初期値とデフォルト値を指定できます。たとえば、ThreadLocalコンストラクターまたはinitialValue()メソッドを使用して初期値を設定できます。

private static ThreadLocal<T> threadLocal = new ThreadLocal<T>() {
    
    
    @Override
    protected T initialValue() {
    
    
        return initialValue;
    }
};

あるいは、ThreadLocal変数を宣言するときにランバダ式を使用してデフォルト値を指定することもできます。

private static ThreadLocal<T> threadLocal = ThreadLocal.withInitial(() -> defaultValue);

3. ThreadLocalのシナリオ例

スレッドコンテキスト情報の受け渡し

  1. コンテキスト情報を作成して保存します。

    • まず、ThreadLocalオブジェクトを作成してコンテキスト情報を保存します。例えば:
      private static ThreadLocal<Context> threadLocal = new ThreadLocal<>();
      
    • コンテキスト情報には、カスタムContextクラスなどの任意のオブジェクト タイプを指定できます。
    • 各スレッドは独立したインスタンスを持つThreadLocalため、ThreadLocalスレッドごとに異なるコンテキスト情報を保存できます。
  2. コンテキスト情報を設定します。

    • コンテキスト情報を設定する必要があるスレッドでは、set()メソッドを使用してコンテキスト情報を現在のスレッドに関連付けます。例えば:
      Context context = new Context(); // 创建上下文信息对象
      threadLocal.set(context); // 设置当前线程的上下文信息
      
  3. コンテキスト情報を取得します。

    • 他のスレッドでは、get()に格納されているコンテキスト情報は、 メソッドを通じて取得されますThreadLocal例えば:
      Context context = threadLocal.get(); // 获取当前线程的上下文信息
      
  4. コンテキスト情報をクリアします:

    • コンテキスト情報が不要になった場合は、メソッドを呼び出して、remove()現在のスレッドのThreadLocalインスタンス内のコンテキスト情報をクリアできます。例えば:
      threadLocal.remove(); // 清除当前线程的上下文信息
      

を使用するとThreadLocal、各スレッドは、他のスレッドのデータに干渉することなく、独自のスレッド スコープ内で独自のコンテキスト情報を保存し、アクセスできます。このスレッドの分離により、ThreadLocalスレッドのコンテキスト情報を効率的に渡すことができます。

次の点に注意してください。

  • 各スレッドは、ThreadLocalコンテキスト情報を保存する必要がある場所に対応する変数を設定する必要があります。これはメソッド内で行うことも、プログラム内の特定の時点で行うこともできます。
  • ThreadLocalの情報が時間内にクリーンアップされないと、メモリ リークが発生する可能性があります。したがって、スレッドへの長期参照を避けるために、 を使用した後ThreadLocal、メソッドを呼び出してクリーンアップする必要があります。remove()
  • ThreadLocalこれはスレッドの安全性の問題を解決するものではなく、スレッド間のデータ分離のメカニズムを提供するだけです。複数のスレッドが同じコンテキスト情報に同時にアクセスする場合でも、スレッドの安全性を確保するために追加の同期メカニズムが必要になります。

スレッドごとに独立したカウンタを実装

  1. オブジェクトを作成しますThreadLocal:

    • まず、ThreadLocalカウンターを格納するオブジェクトを作成します。例えば:
      private static ThreadLocal<Integer> counter = new ThreadLocal<>();
      
  2. カウンタを初期化します。

    • 各スレッドではカウンタの初期値を初期化する必要があります。このステップは、スレッドのエントリポイント、たとえばrun()メソッド内で実行できます。例えば:
      public void run() {
              
              
          counter.set(0); // 初始化计数器为 0
          // 其他操作...
      }
      
  3. カウンタはインクリメントされます。

    • カウントが必要な場合は、ThreadLocalインスタンスを取得し、それに対して自動インクリメント操作を実行できます。例えば:
      int count = counter.get(); // 获取当前线程的计数器值
      count++; // 执行自增操作
      counter.set(count); // 将自增后的值重新设置给当前线程的计数器
      
  4. アクセスカウンター:

    • カウンタの値を取得する必要がある場合は、ThreadLocalインスタンスを通じて現在のスレッドのカウンタ値を取得できます。例えば:
      int count = counter.get(); // 获取当前线程的计数器值
      

上記の手順により、各スレッドが独立したカウンタを持つことができます。各スレッドは独自のThreadLocalインスタンスを持ち、他のスレッドに影響を与えることなく、独自のカウンタ変数を独立して保存およびアクセスできます。

次の点に注意してください。

  • ストア カウンタを使用する場合ThreadLocal、null ポインタ例外やその他の問題を回避するために、使用する前に各スレッドがカウンタを初期化していることを確認する必要があります。
  • 同時実行性の競合を避けるために、カウンターの自己インクリメント操作を同期する必要があります。synchronizedカウンタのアトミックな動作は、キーワードまたはその他の同期メカニズムを使用して保証できます。
  • 各スレッドに対応するカウンターは独立しているため、スレッド間でカウンター値を渡す場合は追加の処理と同期操作が必要になります。

4. ThreadLocalの注意点と使い方スキル

メモリリークの問題と解決策

  1. メモリリークの問題の原因:

    • ThreadLocal保存されたデータはスレッドに関連付けられており、スレッドのライフサイクルは通常比較的長いです。スレッドが終了する前にThreadLocalデータが適切にクリーンアップされないと、メモリ リークが発生します。
    • メモリ リークの主な理由は、各ThreadLocalインスタンスが格納されたデータへの参照を保持し、この参照がスレッドの終了後に自動的に解放されないことです。
  2. メモリ リークの問題を解決する方法:

    • データを時間内にクリーンアップする: 各スレッドが終了する前に、メソッドを手動で呼び出して、そこに保存されているデータをクリーンアップするThreadLocal必要があります。スレッドの終了フックでクリーンアップ操作を実行することも、必要に応じて手動でクリーンアップすることもできます。ThreadLocalremove()
    • ブロックを使用してtry-finallyクリーンアップ操作を確実に実行する: スレッドの終了時にクリーンアップ操作を確実に実行できるようにするには、ブロックを使用してtry-finally関連コードをラップし、例外が発生した場合でもクリーンアップ操作を確実に実行できるようにします。
    • ThreadLocalのサブクラスを使用してremove()メソッドをオーバーライドする:ThreadLocalのサブクラスを作成し、そのremove()メソッドをオーバーライドして、スレッド終了時にデータを自動的にクリーンアップするロジックを実装できます。例えば:
      public class MyThreadLocal<T> extends ThreadLocal<T> {
              
              
          @Override
          public void remove() {
              
              
              // 执行清理操作
              super.remove();
          }
      }
      
    • 弱い参照を使用する (WeakReference):オブジェクトが使用されなくなったときに自動的にガベージ コレクションできるように、ThreadLocalオブジェクトをラップします。WeakReference弱い参照を使用すると、場合によってはデータが不正確に取得される可能性があることに注意してください。

次の点に注意してください。

  • データの保存に使用する場合はThreadLocal、メモリ リークを避けるために、適時にデータをクリーンアップしてください。
  • ThreadLocalインスタンスが保持するデータ オブジェクトが他の場所でも参照されている場合は、ThreadLocalデータをクリーンアップする前に、これらの参照が解放されているか、不要になったことを確認する必要があります。
  • ThreadLocal大量のデータを保存するために使用する場合は、メモリ リソースが過剰にならないように、メモリ使用量を慎重に評価する必要があります。

InheritableThreadLocal の使用

InheritableThreadLocalThreadLocalは、子スレッドが親スレッドのスレッドローカル変数を継承できるようにする のサブクラスです。

  1. オブジェクトを作成しますInheritableThreadLocal:

    • まず、InheritableThreadLocalスレッドローカル変数を格納するオブジェクトを作成します。例えば:
      private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
      
  2. スレッドローカル変数を設定します。

    • どのスレッドでも、InheritableThreadLocalインスタンスのset()メソッドを使用してスレッドローカル変数の値を設定できます。例えば:
      threadLocal.set("value"); // 设置线程本地变量的值
      
  3. スレッドのローカル変数を取得します。

    • 現在のスレッドまたはサブスレッドでは、InheritableThreadLocalインスタンスメソッドを通じてget()スレッド ローカル変数の値を取得できます。子スレッドがローカル変数を手動で設定していない場合は、親スレッドから値を継承します。例えば:
      String value = threadLocal.get(); // 获取线程本地变量的值
      
  4. スレッドのローカル変数をクリアします。

    • スレッドローカル変数をクリアする必要がある場合は、InheritableThreadLocalインスタンスのremove()メソッドを呼び出して変数をクリアできます。例えば:
      threadLocal.remove(); // 清除线程本地变量的值
      

次の点に注意してください。

  • InheritableThreadLocal子スレッドが親スレッドのスレッドローカル変数値を継承できるようにしますが、変数値をすべてのスレッドに共有するわけではありません。各スレッドには、スレッドローカル変数の独立したコピーがまだ存在します。
  • 親スレッドでスレッドローカル変数の値を設定すると、子スレッドはその値を継承します。子スレッドが継承前にスレッドローカル変数の値を手動で設定した場合、子スレッドは親スレッドの値を継承せずに、自分で設定した値を使用します。
  • 子スレッドが継承されたスレッドのローカル変数の値を変更しても、各スレッドにはまだ独立した​​コピーが存在するため、他のスレッドや親スレッドの値には影響しません。
  • InheritableThreadLocalこれは、スレッド間でユーザー認証情報やロケールなどを渡すなど、スレッドまたはタスク間でコンテキスト情報を渡すために使用できます。

InheritableThreadLocal親スレッドと子スレッド間のスレッド ローカル変数の継承と転送は、 を介して実現できます。親スレッドによって設定されたスレッド ローカル変数の値は、子スレッドに継承されます。デフォルトでは、子スレッドは他のスレッドに影響を与えることなく、継承された値を変更できます。ただし、各スレッドには依然として独立したコピーがあり、スレッドローカル変数を変更しても他のスレッドには影響しません。

弱参照と ThreadLocal の関係

弱参照(Weak Reference)はJavaにおける特殊な参照であり、通常の強参照(Strong Reference)とは異なり、ガベージコレクション時に再利用されやすいのが特徴です。むしろ、ThreadLocalこれは Java でスレッドローカル変数を実装するために使用されるメカニズムです。

  1. 弱参照の特徴:

    • 弱い参照オブジェクトは、ガベージ コレクション中にリサイクルされやすくなります。オブジェクトを指す弱い参照がある場合でも、ガベージ コレクションでは、そのオブジェクトが弱い参照によってのみ指されている場合、そのオブジェクトはリサイクルされます。
    • 弱参照は、オブジェクトのライフサイクル管理の問題を解決するためによく使用されます。たとえば、オブジェクトが弱い参照によってのみ参照されている場合、そのオブジェクトは簡単にクリーンアップできます。
  2. ThreadLocal と弱参照の関係:

    • ThreadLocal弱い参照の特性は、メモリ リークの問題の解決に役立ちます。の場合ThreadLocal、スレッドが終了してもThreadLocal時間内にクリーンアップされないと、メモリ リークが発生します。このとき、弱い参照を使用すると、ThreadLocal次回のガベージ コレクションで再利用できるため、メモリ リークの問題が解決されます。
    • JDK の実装では、スレッドのローカル変数を格納するためにThreadLocal内部的に使用されますThreadLocalMapThreadLocalMapのキーはThreadLocalインスタンスであり、値は対応するスレッドローカル変数の値です。そして、ThreadLocalMapキー in は実際には弱参照 ( WeakReference<ThreadLocal<?>>) オブジェクトです。
    • 弱い参照をThreadLocalキーとして使用するThreadLocalと、それを指す他の強い参照がない場合にそれを再利用できるため、メモリ リークの問題が解決されます。

次の点に注意してください。

  • 弱参照を へのキーとして使用する場合、とその格納データが必要なくなったときに、オブジェクトへの強参照がThreadLocal逆参照され、必要に応じてガベージ コレクションできるようにする必要があります。ThreadLocalThreadLocal
  • のライフサイクルと使用状況に注意し、メモリ リークを避けるためにThreadLocal、 と にThreadLocal保存されているデータが適切なタイミングでクリーンアップされるようにしてください。

import java.lang.ref.WeakReference;

public class ThreadLocalExample {
    
    

    private static ThreadLocal<WeakReference<MyObject>> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
    
    
        // 创建一个线程并启动
        Thread thread = new Thread(() -> {
    
    
            MyObject myObject = new MyObject("Thread 1");
            threadLocal.set(new WeakReference<>(myObject)); // 使用弱引用包装对象并设置为线程本地变量值

            // 执行一些操作
            // ...

            myObject = null; // 解除对对象的强引用,让其成为弱引用指向的对象
            System.gc(); // 手动触发垃圾回收

            // ...

            // 在需要使用线程本地变量时,从 ThreadLocal 中获取弱引用并恢复对象
            MyObject retrievedObject = threadLocal.get().get();
            if (retrievedObject != null) {
    
    
                System.out.println(retrievedObject.getName());
            } else {
    
    
                System.out.println("Object has been garbage collected.");
            }
        });

        thread.start();
    }

    static class MyObject {
    
    
        private String name;

        public MyObject(String name) {
    
    
            this.name = name;
        }

        public String getName() {
    
    
            return name;
        }
    }
}

上の例では、ThreadLocalオブジェクトを作成しthreadLocal、その値をWeakReference<MyObject>弱参照として設定しました。スレッドの実行中に、MyObjectオブジェクトが作成されてWeakReference弱参照でラップされ、threadLocalの値に設定されます。

スレッドがいくつかの操作を実行した後、オブジェクトへの強参照を逆参照するようmyObjectに設定します。null次に、ガベージ コレクションを手動でトリガーします。最後にthreadLocalオブジェクトから弱参照を取得して復元し、オブジェクトが空かどうかを判断してガベージコレクションされているかどうかを判断します。

オブジェクトのキーとして弱参照を使用するとThreadLocal、スレッドが終了し、MyObjectオブジェクトへの他の強参照がなくなったときに、オブジェクトはガベージ コレクション中に自動的にクリーンアップされ、メモリ リークが回避されます。

5. 関連する同時実行ツールとフレームワーク

Executor フレームワークでの ThreadLocal の使用法

Executor フレームワークで ThreadLocal を使用すると、スレッド分離されたデータ共有を実現できます。Executor フレームワークは、手動でスレッドを作成および管理することなく、タスクを Executor に送信して実行することにより、Java でスレッドの実行を管理およびスケジュールするためのフレームワークです。

場合によっては、スレッド プール内の異なるスレッド間で一部のデータを共有する必要がある場合がありますが、そのデータが他のスレッドからアクセスされることは望ましくありません。現時点では、ThreadLocal を使用して、Executor フレームワークでスレッド分離データ共有を実装できます。

Executor フレームワークで ThreadLocal を使用する方法を示す例を次に示します。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorThreadLocalExample {
    
    

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

    public static void main(String[] args) {
    
    
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10; i++) {
    
    
            final int taskId = i;
            executorService.execute(() -> {
    
    
                threadLocal.set("Data for task " + taskId);
                System.out.println("Task " + taskId + ": " + threadLocal.get());
                threadLocal.remove(); // 清理 ThreadLocal 的值,防止内存泄漏
            });
        }

        executorService.shutdown();
    }
}

上の例では、固定サイズ 5 のスレッド プールを作成しましたexecutorService次に、execute()このメソッドを使用して 10 個のタスクを送信し、それぞれが匿名の を実行しますRunnable

タスクの実行中に、threadLocalタスクに関連するデータを保存するために使用されます。各タスクでは、タスク固有のデータをthreadLocalの値に設定して出力します。ここでは、各タスクは他のタスクからの干渉を受けることなく、独自の独立したデータを参照します。

を渡すことでthreadLocal.remove()、タスクの完了後に の値をクリーンアップしてthreadLocal、メモリ リークを防ぎます。スレッド プール内のスレッドは再利用されるため、これは非常に重要です。時間内にクリーンアップしないと、スレッドの再利用時にデータの混乱が生じる可能性があります。

Executor フレームワークで ThreadLocal を使用することにより、スレッド分離されたデータ共有を実現できます。各スレッドは、他のスレッドと競合することなく、独自の独立したデータにアクセスして変更できます。これは、スレッドの安全性を維持し、共有データとの競合状態を回避するのに非常に役立ちます。同時に、メモリ リークを避けるために、各タスクの完了後に ThreadLocal 値が確実にクリーンアップされるようにする必要があります。

同時コレクションクラスとThreadLocalの組み合わせ

並行コレクション クラスと ThreadLocal を組み合わせると、マルチスレッド環境でデータのスレッドプライベート化を実現できます。つまり、各スレッドが独立してデータのコピーを持ちます。この併用シナリオには通常、スレッド セーフとデータ分離の要件が含まれます。

Java は、ConcurrentHashMap、ConcurrentLinkedQueue、CopyOnWriteArrayList など、マルチスレッド環境でのパフォーマンスとスレッドの安全性を向上させるさまざまな同時コレクション クラスを提供します。ThreadLocal を使用すると、スレッドごとに変数の独立したコピーを作成でき、スレッド間のデータが相互に干渉しないことが保証されます。

以下は、ThreadLocal での同時コレクション クラスの使用を示す例です。

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentCollectionWithThreadLocalExample {
    
    

    private static ConcurrentHashMap<String, ThreadLocal<Integer>> concurrentMap = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
    
    
        Runnable task = () -> {
    
    
            ThreadLocal<Integer> threadLocal = concurrentMap.computeIfAbsent("counter", k -> ThreadLocal.withInitial(() -> 0));
            int count = threadLocal.get();
            threadLocal.set(count + 1);
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);

        thread1.start();
        thread2.start();
        thread3.start();

        thread1.join();
        thread2.join();
        thread3.join();
    }
}

concurrentMap上の例では、スレッドローカルの ThreadLocal 変数を格納するConcurrentHashMap オブジェクトを作成しました。メインスレッドでは、3 つのスレッドを作成し、タスクを匿名の Runnable として割り当てます。

このタスクでは、まずcomputeIfAbsent()メソッドを介しconcurrentMapて「counter」という名前の ThreadLocal 変数を取得します。変数が存在しない場合は、ThreadLocal.withInitial()メソッドを使用して新しい ThreadLocal 変数を作成し、初期値を 0 に設定します。

次に、get()メソッドを通じて ThreadLocal の値を取得し、値をインクリメントした後に ThreadLocal 変数に戻します。最後に、現在のスレッド名と ThreadLocal 変数の値を出力します。

各スレッドは を通じて独自の独立した ThreadLocal 変数を取得するためcomputeIfAbsent()、各スレッドは独自のカウンタを持ち、互いに干渉しません。

コンカレントコレクションクラスとThreadLocalを組み合わせることで、データの分離と複数スレッド間での独立した計算を実現します。これにより、プログラムのパフォーマンスが向上し、スレッドの安全性が確保されます。

その他のスレッド関連のツールとフレームワーク

ThreadLocal に加えて、Java は、マルチスレッド プログラミングを簡素化し、同時実行制御を実装し、プログラムのパフォーマンスを向上させるために、その他のスレッド関連のツールとフレームワークも提供します。よく使用されるツールとフレームワークをいくつか示します。

  1. Executor フレームワーク: Executor フレームワークは、スレッドの実行を管理およびスケジュールするための Java のフレームワークです。これにより、スレッドを手動で作成および管理することなく、実行のためにタスクをスレッドに送信する方法が提供されます。Executor フレームワークを使用すると、並行プログラミングをより便利に実現できると同時に、スレッド プールのサイズを制御および調整できます。

  2. CompletableFuture: CompletableFuture は、Java 8 で導入された非同期プログラミング ツールです。これは、非同期操作と同時タスクの結果を処理する簡潔な方法を提供します。CompletableFuture は複数のタスクを一緒に構成でき、タスクの完了および例外条件を処理するための豊富なメソッドを提供します。

  3. CountDownLatch: CountDownLatch は、スレッドのグループが一部の操作を完了するのを待機するために使用される同期ヘルパー クラスです。カウンタによって実装されており、カウンタの値がゼロになると待機中のスレッドが解放されます。CountDownLatch は、他のスレッドの初期化が完了するのを待機したり、複数のスレッドが同時に実行を開始するのを待機したりするために、マルチスレッド環境でよく使用されます。

  4. CyclicBarrier: CyclicBarrier は、スレッドのグループが共通のバリア ポイントに到達するのを待機するために使用される別の同期ヘルパー クラスです。CountDownLatch とは異なり、CyclicBarrier は再利用でき、スレッドがバリア ポイントに到達すると、すべてのスレッドが到達するまでブロックされます。すべてのスレッドが到着すると、バリアが開き、スレッドは実行を続行できるようになります。

  5. セマフォ: セマフォは、同時アクセス数を制御するために使用されるカウント セマフォです。リソースにアクセスしたり、コードのブロックを同時に実行したりできるスレッドの数を指定できます。acquire() メソッドと release() メソッドを通じて、スレッドはセマフォを取得および解放できるため、共有リソースに対する制限された制御が実現されます。

  6. ロックと条件: Java のロックと条件インターフェイスは、スレッドの同期と条件付き待機を行うためのより柔軟な方法を提供します。従来の synchronized キーワードと比較して、Lock インターフェイスは、割り込み可能なロック、公平なロック、読み取り/書き込みロックなど、より多くの機能を提供します。Condition インターフェイスはオブジェクトの監視メソッドを拡張し、スレッドが特定の条件が満たされるまで待機し、別のスレッドがシグナルを送信した後に再び起動できるようにします。

  7. Fork/Join フレームワーク: Fork/Join フレームワークは、Java 7 によって導入された並列プログラミング フレームワークで、再帰的な分割統治タスクを効率的に実行するために使用されます。これはワークスチールアルゴリズムに基づいており、タスクをより小さなサブタスクに分解し、ワークキューを介してスレッド間でタスクスチールを実装します。Fork/Join フレームワークは、マルチコア プロセッサの並列コンピューティング機能を最大限に活用して、プログラムのパフォーマンスを向上させることができます。

これらのツールとフレームワークは、さまざまなレベルとドメインのスレッド プログラミング サポートを提供し、実際のニーズに応じて適切なツールを選択して、マルチスレッド プログラミングを簡素化し、同時アクセスを制御し、プログラムの同時パフォーマンスを向上させることができます。

6. パフォーマンスと制限に関する考慮事項

ThreadLocal のパフォーマンスへの影響

  1. メモリ使用量: 各 ThreadLocal 変数のコピーは、一定量のメモリ領域を占有します。作成される ThreadLocal 変数が多すぎて、これらの変数のコピーがほとんどの場合に使用されない場合、追加のメモリ オーバーヘッドが発生します。したがって、ThreadLocal を使用する場合は、作成する必要がある変数の数を合理的に見積もり、未使用の変数を適時にクリーンアップしてメモリ使用量を削減する必要があります。

  2. メモリ リーク: ThreadLocal は変数コピーへの参照を保持するため、ThreadLocal インスタンスが時間内にクリーンアップされない場合、または対応する変数コピーを削除するために Remove() メソッドが呼び出された場合、メモリ リークが発生しやすくなります。特にスレッド プールを使用する場合、ThreadLocal 変数が正しく処理されないと、スレッド プール内のスレッドが変数コピーへの参照を保持し、メモリ リークが発生する可能性があります。

  3. パフォーマンスへの影響: ThreadLocal アクセスは比較的高速ですが、ThreadLocal 変数の使用が多すぎると、同時実行性が高い条件下ではパフォーマンスに悪影響を及ぼす可能性があります。これは、各スレッドが ThreadLocalMap で独自の変数コピーを見つける必要があり、ThreadLocalMap にキーと値のペアが多すぎると検索の効率が低下するためです。また、ThreadLocalMap はハッシュ競合の解決に線形検出を使用するため、競合が多い場合にはアクセスパフォーマンスの低下にもつながります。

さまざまなデータ共有ソリューションを比較する

マルチスレッド プログラミングでは、データ共有が重要な問題になります。さまざまなデータ共有スキームには、それぞれ独自の長所と短所があります。以下では、いくつかの一般的な方法の詳細な紹介と比較を示します。

  1. グローバル変数: グローバル変数は、プログラム全体からアクセスできる変数です。その利点はシンプルかつ直観的であり、どこからでもデータに簡単にアクセスして変更できます。ただし、グローバル変数の欠点は、マルチスレッド環境では競合状態やスレッド セーフティの問題が発生する可能性があり、データの一貫性を確保するには追加の同期メカニズムが必要であることです。

  2. パラメーターの受け渡し: パラメーターの受け渡しは、データ共有の一般的な方法です。各スレッドは、データにアクセスする必要があるメソッドにパラメータを介してデータを渡します。この方法の利点は、スレッド間のデータが独立しており、競合状態やスレッドの安全性の問題が発生しないことです。ただし、複数のメソッドまたは複数レベルの呼び出しが必要な場合、パラメータを渡す方法が複雑で時間がかかる可能性があります。

  3. ThreadLocal: ThreadLocal はスレッド ローカル変数のメカニズムであり、各スレッドは相互に干渉することなく独自の変数のコピーを持ちます。ThreadLocal は、スレッドの閉鎖とデータ共有を実現する使いやすい方法を提供します。これは、一部の特定のシナリオで非常に役立ちます。ただし、ThreadLocal を使用する場合は、メモリ使用量、メモリ リーク、パフォーマンスへの影響などの問題に注意する必要があります。

  4. Synchronized と Lock: synchronized キーワードまたは Lock インターフェイスとその実装クラスを使用して、ロックによる共有データへのマルチスレッド アクセスのセキュリティを確保します。このアプローチでは競合状態やデータの一貫性の問題を回避できますが、デッドロックやパフォーマンスのオーバーヘッドの可能性に注意する必要があります。共有データの読み書きを頻繁に行う場合、粒度が大きすぎたり、ロック時間が長すぎたりすると、プログラムの同時実行性能が低下します。

  5. 同時コレクション クラス: Java は、ConcurrentHashMap、ConcurrentLinkedQueue など、スレッドセーフな同時コレクション クラスをいくつか提供します。これらのコレクション クラスは、マルチスレッド環境でデータを安全に共有できる効率的でスレッドセーフなデータ構造を提供します。同期メカニズムやロック メカニズムと比較すると、通常、同時実行パフォーマンスの点で優れたパフォーマンスが得られます。

一般に、適切なデータ共有ソリューションの選択は、特定のニーズとシナリオに従って検討する必要があります。グローバル変数とパラメータの受け渡し方法はシンプルで簡単ですが、スレッドの安全性の問題をさらに考慮する必要があります。ThreadLocal はスレッドの閉鎖とデータの独立性を実現できますが、メモリ使用量とパフォーマンスへの影響にも注意する必要があります。同期化およびロックによりスレッドの安全性を確保できます。ただし、デッドロックとパフォーマンスの問題に注意を払う必要があります。同時コレクション クラスは、ほとんどの同時シナリオに適した効率的なスレッドセーフ データ構造を提供します。実際の状況に応じて適切な方法を選択し、セキュリティとパフォーマンスのバランスをとって、高品質なマルチスレッド プログラムを作成します。

7. まとめ

ThreadLocal の適用可能なシナリオと適用できないシナリオ

該当するシーン:

  1. スレッド セーフ: 複数のスレッドが同じオブジェクトにアクセスする必要があるが、各スレッドが独自の独立したコピーを維持する必要がある場合、ThreadLocal を使用してスレッド セーフを実現できます。たとえば、Web アプリケーションでは、各リクエストが異なるスレッドによって処理される場合があり、各スレッドはデータベース接続やユーザー ID 情報などに独立してアクセスする必要があります。

  2. スレッドのコンテキスト情報の転送: 場合によっては、ユーザー ID、言語設定などの一部のコンテキスト情報をスレッド間で転送する必要があります。このコンテキスト情報を ThreadLocal に保存すると、この情報をメソッド パラメーターに渡すことを回避でき、メソッドのシグネチャと呼び出しが簡素化されます。

  3. 同じスレッド内の複数のメソッド間でデータを共有する: 同じスレッド内の複数のメソッド間でデータを共有するが、パラメーターを渡したくない場合は、ThreadLocal の使用を検討できます。このようにして、各メソッドはスレッドに依存しないデータのコピーに簡単にアクセスして変更できます。

適用できないシナリオ:

  1. 同時実行性が高い場合の頻繁な更新: 同時実行性が高いシナリオでは、ThreadLocal にパフォーマンスの問題が発生する可能性があります。複数のスレッドが ThreadLocal の値を同時に変更する場合、ロック操作が必要となり、スレッドの競合やパフォーマンスの低下につながる可能性があります。頻繁な更新が必要で、パフォーマンス要件が高い場合は、同時コレクション クラス ConcurrentHashMap など、他のスレッドセーフなデータ構造を使用することをお勧めします。

  2. スレッド間でのデータの受け渡し: ThreadLocal のスコープは現在のスレッドに制限されます。異なるスレッド間でデータを受け渡す必要がある場合、ThreadLocal は機能しません。この場合、スレッド プール内の ConcurrentLinkedQueue や BlockingQueue など、スレッド間で共有メカニズムを使用することを検討できます。

  3. メモリ リーク: ThreadLocal は、使用中のメモリ リークに特別な注意を払う必要があります。ThreadLocal の値が時間内にクリアされない場合、またはスレッドが常にアクティブである場合、ThreadLocal オブジェクトはガベージ コレクションされず、メモリ リークが発生する可能性があります。長時間実行されるアプリケーションでは、ThreadLocal の使用について特別な注意を払う必要があります。

ThreadLocal は、スレッドの閉鎖、スレッドの安全性、およびスレッド間のデータ分離を必要とするシナリオに非常に適しています。ただし、高い同時実行性、頻繁な更新、およびスレッド間でのデータ転送の場合、パフォーマンスの問題が発生したり、要件を満たせない可能性があります。したがって、ThreadLocal を使用するかどうかを選択するときは、特定のシナリオに従って評価し、他のスレッド セーフティ メカニズムとデータ転送方法の実現可能性を考慮する必要があります。

スレッドの安全性とパフォーマンスのバランス

  1. スレッド セーフティの優先順位: ThreadLocal は、スレッド クロージャとスレッド ローカル変数を提供するメカニズムであり、主にマルチスレッド環境におけるデータ セキュリティの問題を解決するために使用されます。パフォーマンスに焦点を当てる前に、まずデータがスレッドセーフであることを確認してください。スレッドの安全性が保証できない場合、パフォーマンスの最適化には意味がありません。

  2. パフォーマンスへの影響に注意してください。ThreadLocal は便利なスレッド クロージャ メカニズムを提供しますが、ThreadLocal を過度に使用したり、ThreadLocal に過度に依存したりすると、メモリ消費量とコンテキスト スイッチング コストが増加し、パフォーマンスに影響を及ぼします。したがって、ThreadLocal を使用する場合は、パフォーマンスへの影響を慎重に評価し、実際のニーズに基づいてトレードオフを行ってください。

  3. 頻繁な更新を避ける: ThreadLocal 値を頻繁に更新すると、パフォーマンスが低下する可能性があります。スレッドの安全性を確保するために、更新ごとにロック操作が必要になるためです。多数の同時更新操作がある場合は、同時コレクション クラス ConcurrentHashMap など、他のスレッドセーフなデータ構造の使用を検討してください。

  4. 計算結果をキャッシュする: ThreadLocal の値が複雑な計算によって得られた場合、初めて値を取得するときに計算を検討し、計算結果を ThreadLocal に保存することができます。これにより、二重計算が回避され、パフォーマンスが向上します。

  5. メモリ リークに注意してください。スレッド間の独立したコピーは ThreadLocal によって維持されるため、不適切に使用するとメモリ リークが発生する可能性があります。オブジェクトのガベージ コレクションの失敗の原因となる無用な参照を避けるために、ThreadLocal を使用するたびに必ず Remove() メソッドを呼び出して変数コピーの値をクリアしてください。

  6. 検討する価値のあるシナリオ: ThreadLocal は、同時実行性が高い、頻繁な更新、またはスレッド間でデータを転送する必要があるシナリオでは要件を満たすことができない可能性があります。この場合、同時コレクション クラスの使用、キューのブロック、メッセージ パッシング メカニズムなど、スレッド セーフとデータ配信の他の方法を考慮する必要があります。

おすすめ

転載: blog.csdn.net/u012581020/article/details/131532012