JPCERT コーディネーションセンター

TPS04-J. スレッドプールの使用時にはThreadLocal変数の再初期化を確実に行う

java.lang.ThreadLocal<T> クラスは、スレッドローカル変数を提供する。Java API では、以下のように説明されている[API 2006]。

これらの変数は、get メソッドと set メソッドを使ってアクセスするスレッドがそれぞれ独自に、変数の初期化されたコピーを持つという点で、通常の変数と異なる。通常、ThreadLocal インスタンスは、状態をスレッドに関連付けようとする場合に、クラスの private static フィールドとして使われる(ユーザID、トランザクションIDなど)。

スレッドプール内の複数のスレッドによって実行される必要があるクラスでは、ThreadLocal オブジェクトの使用に注意が必要である。スレッドプールでは、スレッドを再利用することにより、スレッド生成のオーバーヘッドを軽減したり、スレッドの無制限な生成によるシステムの信頼性低下を防ぐことができる。プールにサブミットされるタスクは、ThreadLocal オブジェクトが、スレッド開始時のデフォルトの状態にあることを期待している。しかし、あるスレッドで ThreadLocal オブジェクトが更新され、そのスレッドが次に再利用されると、そのスレッドで実行されるタスクがアクセスする ThreadLocal オブジェクトの状態は、前の処理で更新されたものになる[JPL 2006]。

スレッドプールのスレッドで実行される各タスクが持つ ThreadLocal オブジェクトは、正しく初期化しなければならない。

違反コード

以下の違反コード例は、曜日を表す列挙型(Day)と2つのクラス(DiaryDiaryPool)から構成されている。Diary クラスでは、各スレッドの実行時の曜日など、そのスレッド固有の情報を格納するために ThreadLocal 変数を使用している。実行時の曜日の初期値は月曜日である。この内容は、後で setDay() メソッドを呼び出すことにより変更可能である。また、このクラスは、スレッド固有のタスクを実行する threadSpecificTask() メソッドも含んでいる。

DiaryPool クラスは、それぞれ1つのスレッドを開始する doSomething1() および doSomething2() メソッドを持っている。doSomething1() メソッドは、曜日の初期値を金曜日に変更し、threadSpecificTask() メソッドを呼び出す。一方 doSomething2() メソッドは、曜日の初期値(月曜日)は変更せず、threadSpecificTask() メソッドを呼び出す。main() メソッドでは、doSomething1() メソッドを使用して1つのスレッドを生成し、doSomething2() メソッドを使ってさらに2つのスレッドを生成している。

public enum Day {
  MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}

public final class Diary {
  private static final ThreadLocal<Day> days =
      new ThreadLocal<Day>() {
  // 日付を月曜日に初期化
    protected Day initialValue() {
      return Day.MONDAY;
    }
  };

  private static Day currentDay() {
    return days.get();
  }

  public static void setDay(Day newDay) {
    days.set(newDay);
  }

 // スレッド固有のタスクを実行する
  public void threadSpecificTask() {
  // タスクの処理を行う ...
  }
}

public final class DiaryPool {
  final int numOfThreads = 2; // プール内の最大スレッド数
  final Executor exec;
  final Diary diary;

  DiaryPool() {
    exec = (Executor) Executors.newFixedThreadPool(numOfThreads);
    diary = new Diary();
  }

  public void doSomething1() {
    exec.execute(new Runnable() {
        @Override public void run() {
          diary.setDay(Day.FRIDAY);
          diary.threadSpecificTask();
        }
    });
  }

  public void doSomething2() {
    exec.execute(new Runnable() {
        @Override public void run() {
          diary.threadSpecificTask();
       }
    });
  }

  public static void main(String[] args) {
    DiaryPool dp = new DiaryPool();
    dp.doSomething1(); // スレッド1は、実行時の曜日として金曜日を指定
    dp.doSomething2(); // スレッド2は、実行時の曜日として月曜日を指定
    dp.doSomething2(); // スレッド3は、実行時の曜日として月曜日を指定
  }
}

DiaryPool クラスは、固定数のスレッドを再利用するスレッドプールを作成する。このスレッドプールのスレッドは、長さに制限のないタスクキューを共有している。numOfThreads が示すスレッド数が、同時にアクティブとなるスレッドの最大数である。スレッドがすべてアクティブな時に追加のタスクが登録された場合、スレッドが利用可能になるまで、それらのタスクはキュー内で待機状態となる。スレッドが再利用されるとき、スレッドローカル変数の状態は保持されたままである。

以下の表では、起こりうるタスクの順序を示している。

時間 タスク プールスレッド タスクを登録したメソッド 曜日
1 t1 1 doSomething1() 金曜日
2 t2 2 doSomething2() 月曜日
3 t3 1 doSomething2() 金曜日

上記の実行順序では、doSomething2() メソッドから開始した2つのタスク(t2 およびt3)の実行時の曜日(変数 days の値)は月曜日になっていることが期待される。しかし、スレッド1が再利用されるので、t3 の実行時の曜日は金曜日になってしまっている。

違反コード (スレッドプールサイズの増加)

以下の違反コード例では、前述の問題の対策として、スレッドプールのサイズを2から3に変更している。

public final class DiaryPool {
  final int numOfthreads = 3;
  // ...
}

スレッドプールのサイズを増加させることにより前述の問題は解決する。しかし、より多くのタスクがプールにサブミットされる場合には、この変更では不十分となり、スケーラビリティに優れた解決方法とはいえない。

適合コード (try-finally ブロック)

以下の適合コードでは、Diary クラスに removeDay() メソッドを追加し、DiaryPool クラスの doSomething1() メソッドの中身を try-finally ブロックで囲んでいる。finally ブロックは、スレッドローカルな days オブジェクトから実行中のスレッドが加えた変更を取り除き、オブジェクトを初期化する。

public final class Diary {
  // ...
  public static void removeDay() {
    days.remove();
  }
}

public final class DiaryPool {
  // ...

  public void doSomething1() {
      exec.execute(new Runnable() {
        @Override public void run() {
          try {
            Diary.setDay(Day.FRIDAY);
            diary.threadSpecificTask();
          } finally {
            Diary.removeDay(); // Diary.setDay(Day.MONDAY) 
                             // メソッドも使用可能
          }
        }
    });
  }

  // ...
}

スレッドローカル変数の値が同一スレッドにより再び読み取られる場合、スレッドが明示的に変数の値をセットしていなければ、initialValue() メソッドにより再度初期化される[API 2006]。上記の解決方法では、スレッドローカル変数の操作に関する責任がクライアント側(DiaryPool)に移されている。この手法は Diary クラスの修正が不可能な場合にはよい解決策である。

適合コード (beforeExecute() メソッド)

以下の適合コードでは、ThreadPoolExecutor クラスを拡張した CustomThreadPoolExecutor クラスを定義し、beforeExecute() メソッドをオーバーライドしている。この beforeExecute() メソッドは、指定したスレッド内で Runnable タスクが実行される前に呼び出される。beforeExecute() メソッドは、タスク r がスレッド t で実行される前に、スレッドローカル変数を再初期化する。

class CustomThreadPoolExecutor extends ThreadPoolExecutor {
  public CustomThreadPoolExecutor(int corePoolSize,
      int maximumPoolSize, long keepAliveTime,
      TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, 
          unit, workQueue);
  }

  @Override
  public void beforeExecute(Thread t, Runnable r) {
    if (t == null || r == null) {
      throw new NullPointerException();
    }
    Diary.setDay(Day.MONDAY);
    super.beforeExecute(t, r);
  }
}

public final class DiaryPool {
  // ...
  DiaryPool() {
    exec = new CustomThreadPoolExecutor(NumOfthreads, NumOfthreads,
               10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));
    diary = new Diary();
  }
  // ...
}
例外

TPS04-EX0: 初期化処理後に状態が変更されない ThreadLocal オブジェクトを再度初期化する必要はない。たとえば、ThreadLocal 変数にデータベース接続オブジェクトを設定した場合、初期設定以降その値を変更する必要はないだろう。

リスク評価

ThreadLocal データを持つオブジェクトが、再初期化を行わずにスレッドプールの異なるタスクで実行されると、スレッドの再利用によって予期せぬ状態になることがありうる。

ルール 深刻度 可能性 修正コスト 優先度 レベル
TPS04-J P4 L3
参考文献
[API 2006] Class java.lang.ThreadLocal<T>
[JPL 2006] 14.13, ThreadLocal Variables
翻訳元

これは以下のページを翻訳したものです。

TPS04-J. Ensure ThreadLocal variables are reinitialized when using thread pools (revision 57)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter