Th-04: ThreadLocal — スレッド固有のデータ保持
ThreadLocal を使うと、同じ変数宣言でも各スレッドが独立したコピーを持ちます。 スレッドプール環境でのリクエストスコープ管理や、スレッドアンセーフなSimpleDateFormat を安全に使うための手法として広く利用されています。
ThreadLocal とは何か
通常のフィールドは全スレッドで共有されますが、ThreadLocal<T> で宣言したフィールドは スレッドごとに独立したコピーを持ちます。スレッド A が set(1) しても、 スレッド B の get() には影響しません。
// スレッドごとに独立した Integer を保持 private static final ThreadLocal<Integer> holder = new ThreadLocal<>(); // スレッド A: 1 をセット holder.set(1); // スレッド B: null(スレッド A の値は見えない) holder.get(); // → null // 使い終わったら必ず remove()(メモリリーク防止) holder.remove();
主な実用例
| 用途 | 説明 |
|---|---|
| リクエストスコープ | Web アプリでリクエストごとのユーザー ID・認証情報を保持する |
| SimpleDateFormat の安全化 | スレッドアンセーフなオブジェクトをスレッドごとに1つ用意する |
| トランザクション管理 | DB 接続(Connection)をスレッドに紐づけて引き回す |
サンプルコード
よくあるミス・注意点
⚠️ remove() を忘れるとスレッドプール環境でメモリリークが発生する
スレッドプール(Tomcat のリクエスト処理スレッドなど)では、スレッドが終了せずに再利用されます。remove() を呼ばないと前のリクエストの値が残り続け、次のリクエストに意図しないデータが流れ込む原因になります。 さらに、古い値がガベージコレクションされずメモリリークにもつながります。ThreadLocal を使う場合は、処理が終わったら必ず remove() を呼んでください。
// ❌ remove() を忘れると前回の値が残る
userIdHolder.set(userId);
// ... 処理 ...
// remove() を呼ばずにスレッドが次のリクエストに再利用される
// ✅ 使い終わったら必ず remove()
try {
userIdHolder.set(userId);
// ... 処理 ...
} finally {
userIdHolder.remove(); // finally で確実に解放
}⚠️ 子スレッドには ThreadLocal の値が引き継がれない
親スレッドで set() した値は、new Thread() で作った子スレッドには引き継がれません。 子スレッドに値を渡したい場合は InheritableThreadLocal を使うか、 コンストラクタ引数で明示的に渡してください。
テストする観点
- ✅ 複数スレッドから同時にアクセスしても、各スレッドが自分の userId のみを取得できること(値が混在しないこと)
- ✅
remove()を呼んだ後、同じスレッドでget()するとnullが返ること - ✅
ThreadLocal.withInitial()で設定した初期値が、最初のget()で返ること - ✅ 親スレッドで
set()した値は、new Thread()で作った子スレッドには引き継がれないこと - ✅
InheritableThreadLocalでは子スレッドに値が引き継がれること