java-recipes

ホーム マルチスレッド › Th-04

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)をスレッドに紐づけて引き回す

サンプルコード

Sample.java
import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadLocalSample {

    // ✅ ThreadLocal: 各スレッドに独立したデータを保持
    private static final ThreadLocal<Integer> userIdHolder = new ThreadLocal<Integer>();

    // 実用例: SimpleDateFormat はスレッドアンセーフ → ThreadLocal で解決
    private static final ThreadLocal<SimpleDateFormat> dateFormatHolder =
        new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd");
            }
        };

    // リクエストコンテキストの模擬
    public static void setUserId(int id) {
        userIdHolder.set(id);
    }

    public static Integer getUserId() {
        return userIdHolder.get();
    }

    public static void clearUserId() {
        userIdHolder.remove(); // ✅ 必ず remove() する(メモリリーク防止)
    }

    public static String formatDate(Date date) {
        return dateFormatHolder.get().format(date);
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== ThreadLocal でスレッド固有データを保持 ===");

        Runnable task = new Runnable() {
            @Override
            public void run() {
                int threadId = (int) (Thread.currentThread().getId() % 1000);
                setUserId(threadId);

                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }

                // 他スレッドのデータに影響されない
                System.out.println(Thread.currentThread().getName()
                    + " → userId=" + getUserId());
                clearUserId(); // ✅ 使い終わったら必ず remove()
            }
        };

        Thread t1 = new Thread(task, "thread-A");
        Thread t2 = new Thread(task, "thread-B");
        Thread t3 = new Thread(task, "thread-C");
        t1.start(); t2.start(); t3.start();
        t1.join(); t2.join(); t3.join();

        System.out.println("\n=== SimpleDateFormat の ThreadLocal 解決策 ===");
        Runnable formatTask = new Runnable() {
            @Override
            public void run() {
                String result = formatDate(new Date());
                System.out.println(Thread.currentThread().getName() + " → " + result);
            }
        };
        Thread f1 = new Thread(formatTask, "format-1");
        Thread f2 = new Thread(formatTask, "format-2");
        f1.start(); f2.start();
        f1.join(); f2.join();
    }
}

よくあるミス・注意点

⚠️ 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 では子スレッドに値が引き継がれること

GitHub でソースコードを見る →