JavaのThreadLocalの概要
1. 概要
この記事では、java.langパッケージからのThreadLocalコンストラクトを見ていきます。 これにより、現在のスレッドのデータを個別に保存し、特別なタイプのオブジェクトに単純にラップすることができます。
2. ThreadLocal API
TheadLocal構文を使用すると、accessible only xa specific threadになるデータを格納できます。
特定のスレッドにバンドルされるInteger値が必要だとします。
ThreadLocal threadLocalValue = new ThreadLocal<>();
次に、スレッドからこの値を使用する場合は、get()またはset()メソッドを呼び出すだけで済みます。 簡単に言えば、ThreadLocalは、スレッドをキーとして、マップ内にデータを格納していると考えることができます。
そのため、threadLocalValueでget()メソッドを呼び出すと、要求しているスレッドのInteger値が取得されます。
threadLocalValue.set(1);
Integer result = threadLocalValue.get();
withInitial()静的メソッドを使用し、それにサプライヤーを渡すことにより、ThreadLocalのインスタンスを構築できます。
ThreadLocal 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()メソッドの実装は、指定されたuserIdのContextオブジェクトを返すUserRepositoryクラスを介してデータベースを呼び出します。
次に、そのコンテキストをuserIdでキー設定されたConcurentHashMapに格納します。
public class SharedMapWithUserContext implements Runnable {
public static Map 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 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. ExecutorServiceと一緒にThreadLocalを使用しないでください
ExecutorServiceを使用してRunnableを送信する場合、ThreadLocalを使用すると、すべてのRunnableアクションが保証されないため、非決定論的な結果が得られます。指定されたuserIdは、実行されるたびに同じスレッドによって処理されます。
そのため、ThreadLocalは異なるuserIds.間で共有されます。そのため、TheadLocalをExecutorService.と一緒に使用しないでください。完全に制御できる場合にのみ使用してください。どのスレッドが実行する実行可能なアクションを選択するか。
6. 結論
この簡単な記事では、ThreadLocalコンストラクトを見ていました。 スレッド間で共有されたConcurrentHashMapを使用して、特定のuserId.に関連付けられたコンテキストを格納するロジックを実装しました。次に、ThreadLocalを活用して、に関連付けられたデータを格納するように例を書き直しました。特定のuserIdと特定のスレッド。
これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。