Java 逆引き集 | ThreadLocal を使ったスレッド依存データ管理 — スレッド単位の状態

Java Java
スポンサーリンク

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(); // スレッドごとに独立
Java

try-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)を使う、Executorsubmit でコンテキストをクロージャに閉じるなど。
  • 非同期境界に注意: 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 のスコープ)と比較してから採用するのが安全策。
タイトルとURLをコピーしました