1. 概要

このチュートリアルでは、 java.langパッケージのThreadLocalコンストラクトを見ていきます。 これにより、現在のスレッドのデータを個別に保存し、それを特別なタイプのオブジェクトにラップすることができます。

2. ThreadLocal API

TheadLocal コンストラクトを使用すると、特定のスレッドによってのみアクセスできるデータを格納できます。

特定のスレッドにバンドルされるInteger値が必要だとします。

ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

次に、スレッドからこの値を使用する場合は、 get()または set()メソッドを呼び出すだけで済みます。 簡単に言えば、 ThreadLocal は、スレッドをキーとしてマップ内にデータを格納することを想像できます。

その結果、 threadLocalValueget()メソッドを呼び出すと、要求元のスレッドのInteger値が取得されます。

threadLocalValue.set(1);
Integer result = threadLocalValue.get();

withInitial()静的メソッドを使用し、それにサプライヤーを渡すことで、ThreadLocalのインスタンスを構築できます。

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

ThreadLocal から値を削除するには、 remove()メソッドを呼び出すことができます。

threadLocal.remove();

ThreadLocal を適切に使用する方法を確認するために、最初に ThreadLocal を使用しない例を見てから、その構成を活用するように例を書き直します。

3. マップへのユーザーデータの保存

特定のユーザーIDごとにユーザー固有のContextデータを保存する必要があるプログラムを考えてみましょう。

public class Context {
    private String userName;

    public Context(String userName) {
        this.userName = userName;
    }
}

ユーザーIDごとに1つのスレッドが必要です。 Runnableインターフェイスを実装するSharedMapWithUserContextクラスを作成します。 run()メソッドの実装は、特定のuserIdContextオブジェクトを返すUserRepositoryクラスを介してデータベースを呼び出します。

次に、そのコンテキストをuserIdをキーとするConcurentHashMapに格納します。

public class SharedMapWithUserContext implements Runnable {
 
    public static Map<Integer, Context> userContextPerUserId
      = new ConcurrentHashMap<>();
    private Integer userId;
    private UserRepository userRepository = new UserRepository();

    @Override
    public void run() {
        String userName = userRepository.getUserNameForUserId(userId);
        userContextPerUserId.put(userId, new Context(userName));
    }

    // standard constructor
}

2つの異なるuserIds、に対して2つのスレッドを作成して開始し、 userContextPerUserId マップに2つのエントリがあることを表明することで、コードを簡単にテストできます。

SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);

4. ThreadLocalへのユーザーデータの保存

ThreadLocal を使用して、ユーザーContextインスタンスを格納するように例を書き直すことができます。 各スレッドには、独自のThreadLocalインスタンスがあります。

ThreadLocal を使用する場合、すべての ThreadLocal インスタンスが特定のスレッドに関連付けられているため、十分に注意する必要があります。 この例では、特定の userId ごとに専用のスレッドがあり、このスレッドは私たちが作成したものなので、完全に制御できます。

run()メソッドは、ユーザーコンテキストをフェッチし、 set()メソッドを使用してThreadLocal変数に格納します。

public class ThreadLocalWithUserContext implements Runnable {
 
    private static ThreadLocal<Context> userContext 
      = new ThreadLocal<>();
    private Integer userId;
    private UserRepository userRepository = new UserRepository();

    @Override
    public void run() {
        String userName = userRepository.getUserNameForUserId(userId);
        userContext.set(new Context(userName));
        System.out.println("thread context for given userId: " 
          + userId + " is: " + userContext.get());
    }
    
    // standard constructor
}

特定のuserIdのアクションを実行する2つのスレッドを開始することで、テストできます。

ThreadLocalWithUserContext firstUser 
  = new ThreadLocalWithUserContext(1);
ThreadLocalWithUserContext secondUser 
  = new ThreadLocalWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

このコードを実行すると、標準出力にThreadLocalが特定のスレッドごとに設定されていることがわかります。

thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'}
thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}

各ユーザーが独自のコンテキストを持っていることがわかります。

5. ThreadLocalとスレッドプール

ThreadLocal は、いくつかの値を各スレッドに制限するための使いやすいAPIを提供します。 これは、Javaでスレッドセーフを実現するための合理的な方法です。 ただし、 ThreadLocal とスレッドプールを一緒に使用する場合は、特に注意する必要があります。

この考えられる警告をよりよく理解するために、次のシナリオを考えてみましょう。

  1. まず、アプリケーションはプールからスレッドを借用します。
  2. 次に、スレッドに制限された値を現在のスレッドのThreadLocalに格納します。
  3. 現在の実行が終了すると、アプリケーションは借用したスレッドをプールに返します。
  4. しばらくすると、アプリケーションは同じスレッドを借用して別のリクエストを処理します。
  5. アプリケーションは前回必要なクリーンアップを実行しなかったため、新しいリクエストに同じThreadLocalデータを再利用する可能性があります。

これは、同時実行性の高いアプリケーションで驚くべき結果を引き起こす可能性があります。

この問題を解決する1つの方法は、使用が終了したら、各ThreadLocalを手動で削除することです。 このアプローチには厳密なコードレビューが必要なため、エラーが発生しやすくなります。

5.1. ThreadPoolExecutorの拡張

それが判明したとして、 ThreadPoolExecutorクラスを拡張し、beforeExecute()メソッドとafterExecute()メソッドのカスタムフック実装を提供することができます。 スレッドプールはを呼び出します beforeExecute() 借用したスレッドを使用して何かを実行する前のメソッド。 一方、ロジックを実行した後、 afterExecute()メソッドを呼び出します。

したがって、 ThreadPoolExecutor クラスを拡張し、 afterExecute()メソッドのThreadLocalデータを削除できます。

public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor {

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        // Call remove on each ThreadLocal
    }
}

このExecutorServiceの実装にリクエストを送信すると、ThreadLocalとスレッドプールを使用してもアプリケーションに安全上の問題が発生しないことを確認できます。

6. 結論

この短い記事では、ThreadLocalコンストラクトを調べました。 を使用するロジックを実装しました ConcurrentHashMap 特定のに関連付けられたコンテキストを格納するためにスレッド間で共有されたユーザーID。 次に、例を書き直して活用します ThreadLocal 特定のデータに関連付けられたデータを保存するユーザーID そして特定のスレッド。

これらすべての例とコードスニペットの実装は、GitHubにあります。