ThreadLocal を使ったスレッド依存データ管理 — スレッド単位の状態
同じ変数名でも、各スレッドが「自分専用の値」を持てるのが ThreadLocal。リクエスト単位のトレースID、フォーマッタやバッファの再利用、認証コンテキストなど「スレッド境界で分離したい状態」に向いています。ただし使いどころとクリーンアップを間違えるとメモリリークやバグの温床になります。
基本の考え方
- スレッドローカルの性質: 同一 ThreadLocal でもスレッドごとに独立値。ほかのスレッドには見えない。
- 代表 API:
ThreadLocal<T> tl = ThreadLocal.withInitial(Supplier<T>)初期値つき生成tl.get()取得、tl.set(value)設定、tl.remove()削除(必須)
- 適用場面:
- リクエスト/処理単位のコンテキスト(トレースID、ユーザー情報)
- スレッドごとの再利用オブジェクト(フォーマッタ/バッファ)
- ランダム(
ThreadLocalRandomは内部的に類似コンセプト)
すぐ使える基本例
初期値つき ThreadLocal(withInitial)
import java.util.concurrent.atomic.AtomicInteger;
class RequestContext {
static final ThreadLocal<String> traceId = ThreadLocal.withInitial(() -> "NA");
static final ThreadLocal<AtomicInteger> counter = ThreadLocal.withInitial(AtomicInteger::new);
}
// 利用
RequestContext.traceId.set("REQ-123");
int now = RequestContext.counter.get().incrementAndGet(); // スレッドごとに独立
Javatry-finally で必ず remove(リーク防止)
ThreadLocal<String> user = new ThreadLocal<>();
try {
user.set("tanaka");
// ここで処理
} finally {
user.remove(); // スレッドプール環境では特に必須
}
Javaスレッド安全でないフォーマッタを ThreadLocal で共有
import java.text.SimpleDateFormat;
import java.util.Date;
class Fmt {
static final ThreadLocal<SimpleDateFormat> sdf =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
String ts = Fmt.sdf.get().format(new Date()); // 各スレッドで独立インスタンス
Java例題で身につける
例題1: リクエストIDをロギングに自動付与
class LogCtx {
static final ThreadLocal<String> reqId = new ThreadLocal<>();
}
void handle(String id) {
try {
LogCtx.reqId.set(id);
log("start"); // → "REQ-xyz start"
// ... 処理 ...
log("end"); // → "REQ-xyz end"
} finally {
LogCtx.reqId.remove(); // スレッドプール再利用に備える
}
}
void log(String msg) {
String id = LogCtx.reqId.get();
System.out.println((id != null ? id : "NA") + " " + msg);
}
Java例題2: スレッド別の蓄積バッファ
class Buf {
static final ThreadLocal<StringBuilder> sb = ThreadLocal.withInitial(StringBuilder::new);
}
String buildLine(String s) {
StringBuilder b = Buf.sb.get();
b.setLength(0); // 既存を再利用(GC負荷を減らす)
b.append("[line] ").append(s);
return b.toString();
}
Java例題3: 子スレッドにも初期値を渡したい(InheritableThreadLocal)
class Ctx {
static final InheritableThreadLocal<String> lang = new InheritableThreadLocal<>();
}
void parent() {
Ctx.lang.set("ja-JP");
new Thread(() -> {
// 子スレッドで "ja-JP" を引き継いでいる
System.out.println(Ctx.lang.get());
Ctx.lang.remove();
}).start();
Ctx.lang.remove();
}
Java実務のコツ
- 必ず remove する: スレッドプールはスレッドを再利用するため、ThreadLocal の値が次のリクエストに残留しがち。try-finally で確実に消す。
- スコープを小さく: 値設定は「処理の入り口」で、削除は「出口」で。ヘルパー/フィルタで一箇所にまとめる。
- 代替を考える: 共有状態が本当に必要か再考。メソッド引数で渡す、DI コンテナのスコープ(request/session)を使う、
Executorのsubmitでコンテキストをクロージャに閉じるなど。 - 非同期境界に注意: ThreadLocal の値は「スレッド間」で自動伝播しない。別スレッドに処理を渡すときは値を取り出して明示的に渡す。
- デバッグ難易度: グローバルに見えるのにスレッドで分離されるため、ログにスレッド名/ID/トレースIDを必ず含める。
- サイズの大きいオブジェクトは避ける: 大きなキャッシュやコレクションを ThreadLocal に入れるとメモリを食う。用途は軽量オブジェクトに限定。
よくある落とし穴と回避策
- メモリリーク(remove しない): スレッドが長寿命だと、ThreadLocalMap に値が残り続ける。必ず finally で
remove()。 - キーの弱参照誤解: ThreadLocal のキーは GC 対象になり得るが、値が残るケースがありうる。やはり
remove()が最善策。 - 値の流用バグ: スレッドプールで前回リクエストの値が次回に見える事故。スコープ化して「必ず設定/必ず削除」。
- 非同期/並列での伝播期待:
CompletableFutureや別Executorに渡すと値が見えない。必要なら値を引数で渡すか、伝播機構を自作する。 - セキュリティ情報の残留: 認証情報を ThreadLocal に入れるならなおさら remove を徹底。漏れは重大事故。
テンプレート集(そのまま使える形)
- 初期値つき定義
static final ThreadLocal<T> TL = ThreadLocal.withInitial(() -> initial());
Java- スコープ管理(ヘルパー)
final class Scoped<T> {
private final ThreadLocal<T> tl = new ThreadLocal<>();
void runWith(T v, Runnable r) {
try { tl.set(v); r.run(); } finally { tl.remove(); }
}
T get() { return tl.get(); }
}
Java- try-finally 定型
try {
TL.set(val);
// do work
} finally {
TL.remove();
}
Java- 非同期へ渡す(明示伝播)
String id = TL.get();
executor.submit(() -> {
try { TL.set(id); task.run(); } finally { TL.remove(); }
});
Java- スレッド安全でないクラスの保護
static final ThreadLocal<SimpleDateFormat> SDF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
Javaまとめ
- ThreadLocal は「スレッドごとの独立状態」を簡潔に提供する。使いどころはリクエストコンテキストや非スレッドセーフオブジェクトの再利用。
- スレッドプール環境では
try-finallyでのremove()が絶対必須。非同期境界では値は自動伝播しないため、必要なら明示的に渡す。 - スコープを小さく、用途を軽量に保ち、代替手段(引数、DI のスコープ)と比較してから採用するのが安全策。
