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

THI02-J. 1つではなくすべての待ち状態スレッドへ通知を行う

Object.wait()メソッドを呼び出すスレッドは、その条件述語が真になったとき、待機状態から復帰して実行を再開する。「THI03-J. wait() および await() メソッドは常にループ内部で呼び出す」に適合するため、待機状態のスレッドは、通知を受信したときに条件述語を評価し、偽である場合には再度待機状態に戻る必要がある。

java.lang.Objectクラスのnotify()メソッドおよびnotifyAll()メソッドを使用することで、待機中のスレッドを再開することができる。これらのメソッドの呼出しは、待機スレッドが保持するのと同じオブジェクトの固有ロックを保持するスレッドから行わなくてはならない。その他のスレッドから呼び出された場合、IllegalMonitorStateException例外をスローする。notifyAll()メソッドは、すべてのスレッドを待機状態から復帰させ、そのなかで条件述語が真であるスレッドの実行再開を可能にする。さらに、条件述語が真であるスレッドがすべて、待機状態に入る前に特定のロックを獲得していたとしても、1つのスレッドだけが、通知を受信したときにロックを再度獲得する。その場合、他のスレッドは待機状態に戻るであろう。notify()メソッドは、1つのスレッドを待機状態から復帰させるが、どのスレッドへ通知が行われるかは保証しない。通知を受け取ったスレッドは、条件述語が偽である場合には再度待機するかもしれず、これは本来の通知の意図に反する。

したがって、次に示す条件をすべて満たす場合にのみ、notify()メソッドを呼び出すべきである。

処理内容が等しく、ステートレスなサービスやユーティリティを提供するスレッドは、これらの条件を満たしている。

java.util.concurrent.locksパッケージは、Condition.await()メソッドの呼出しでブロックしているスレッドを復帰させるために、Condition.signal()メソッドおよびCondition.signalAll()メソッドを提供している。java.util.concurrent.locks.Lockオブジェクトを使用する場合、Conditionオブジェクトが必要である。LockオブジェクトはObject.wait(), Object.notify(), Object.notifyAll()メソッドの使用を可能にするが、これらのメソッドの使用は「LCK03-J. 高水準な並行処理オブジェクトの固有ロックを使って同期を行わない」により禁止されている。Lockオブジェクトを用いて同期を行うコードは、それ自身の固有ロックは使用せずに、Lockオブジェクトと関連付けされた1つ以上のConditionオブジェクトを代わりに使用する。これらの Condition オブジェクトは、Lock オブジェクトで強制されるロックポリシーと直接やり取りする。したがって、wait()notify()notifyAll()メソッドの代わりに、await()signal()signalAll()が使用される。

次に挙げる条件をすべて満さない限り、signal()メソッドを使用してはならない。

あるいは、次に示す条件をすべて満たさない限り、signal()を使用してはならない。

安全に使用するならば、signal()メソッドはsignalAll()メソッドよりも性能面で優れている。

違反コード (notify())

以下の違反コードでは、複数のスレッドが、複数の処理ステップから構成される複雑なプロセスを実行している。各スレッドは、timeフィールドの値に対応する処理ステップを実行する。各スレッドは、担当するステップを実行した後、timeをインクリメントし、次のステップを担当するスレッドへ通知する。

public final class ProcessStep implements Runnable {
  private static final Object lock = new Object();
  private static int time = 0;
  private final int step; // timeフィールドがこの値に達したら
                          // 処理を実行する

  public ProcessStep(int step) {
    this.step = step;
  }

  @Override public void run() {
    try {
      synchronized (lock) {
        while (time != step) {
          lock.wait();
        }

        // 処理を実行する

        time++;
        lock.notify();
      }
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt(); // 割込みステータスをリセットする
    }
  }

  public static void main(String[] args) {
    for (int i = 4; i >= 0; i--) {
      new Thread(new ProcessStep(i)).start();
    }
  }
}

上記のコードは、活性(liveness)の要件を満たしていない。各スレッドはそれぞれ異なるstepの値に対して処理を実行するので、各スレッドの条件述語(condition predicate)は異なる。Object.notify()メソッドは、一度に1つのスレッドしか復帰させないので、復帰したスレッドが次に実行予定のステップを担当するスレッドでない限り、プログラムはデッドロックに陥る。

適合コード (notifyAll())

以下の適合コードでは、各スレッドは担当するステップを処理した後、待機スレッドへ通知を行うためにnotifyAll()メソッドを呼び出している。条件述語が真(ループ条件式の判定結果としては偽)となるスレッドは、その時点で自身の担当ステップを実行することが可能であるが、条件述語が偽のスレッドは待機状態に戻る。

前述の違反コードのrun()メソッドだけを、以下のように修正している。

public final class ProcessStep implements Runnable {
  private static final Object lock = new Object();
  private static int time = 0;
  private final int step; // timeフィールドがこの値に達したら
                          // 処理を実行する
  public ProcessStep(int step) {
    this.step = step;
  }

  @Override public void run() {
    try {
      synchronized (lock) {
        while (time != step) {
          lock.wait();
        }
  
        // 処理を実行する
  
        time++;
        lock.notifyAll(); // notify()の代わりにnotifyAll()を使用
      }
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt(); // 割込みステータスをリセット
    }
  }

}
違反コード (Condition インタフェース)

以下の違反コードは、notify()メソッドを使用した違反コードに類似しているが、待機と通知のためにConditionインタフェースを使用している。

public class ProcessStep implements Runnable {
  private static final Lock lock = new ReentrantLock();
  private static final Condition condition = lock.newCondition();
  private static int time = 0;
  private final int step; // timeフィールドがこの値に達したら
                          // 処理を実行する
  public ProcessStep(int step) {
    this.step = step;
  }

  @Override public void run() {
    lock.lock();
    try {
      while (time != step) {
        condition.await();
      }

      // 処理を実行する

      time++;
      condition.signal();
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt(); // 割込みステータスをリセットする
    } finally {
      lock.unlock();
    }
  }

  public static void main(String[] args) {
    for (int i = 4; i >= 0; i--) {
      new Thread(new ProcessStep(i)).start();
    }
  }
}

Object.notify()メソッドを使った場合と同様に、signal()メソッドの呼出しでどのスレッドが復帰するかは分からない。

適合コード (signalAll())

以下の適合コードでは、signalAll()メソッドを使用してすべての待機スレッドへの通知を行っている。各スレッドは、await()メソッドから戻る前にConditionオブジェクトに関連付けられたロックを再び取得する。復帰したスレッドは、このロックを保持していることが保証されている [API 2006]。条件述語が真となる状態のスレッドは、担当のタスクを実行することが可能であるが、条件述語が偽である他のスレッドはすべて待機状態に戻る。

前述の違反コードのrun()メソッドのみを以下のように修正している。

public class ProcessStep implements Runnable {
  private static final Lock lock = new ReentrantLock();
  private static final Condition condition = lock.newCondition();
  private static int time = 0;
  private final int step; // timeフィールドがこの値に達したら
                          // 処理を実行する
  public ProcessStep(int step) {
    this.step = step;
  }

  @Override public void run() {
    lock.lock();
    try {
      while (time != step) {
        condition.await();
      }
  
      // 処理を実行する

      time++;
      condition.signalAll();
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt(); // 割込みステータスをリセットする
    } finally {
      lock.unlock();
    }
  }

}
適合コード(スレッドごとに一意の Condition オブジェクト)

以下の適合コードでは、各スレッドに独自のConditionオブジェクトを割り当てている。すべてのConditionオブジェクトは、全スレッドからアクセス可能である。

// コンストラクタが例外をスローするので、このクラスではfinal宣言している
public final class ProcessStep implements Runnable {
  private static final Lock lock = new ReentrantLock();
  private static int time = 0;
  private final int step; // timeフィールドがこの値に達したら
                          // 処理を実行する
  private static final int MAX_STEPS = 5;
  private static final Condition[] conditions = new Condition[MAX_STEPS];

  public ProcessStep(int step) {
    if (step <= MAX_STEPS) {
      this.step = step;
      conditions[step] = lock.newCondition();
    } else {
      throw new IllegalArgumentException("Too many threads");
    }
  }

  @Override public void run() {
    lock.lock();
    try {
      while (time != step) {
        conditions[step].await();
      }

      // 処理を実行する

      time++;
      if (step + 1 < conditions.length) {
        conditions[step + 1].signal();
      }
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt(); // 割込みステータスをリセットする
    } finally {
      lock.unlock();
    }
  }

  public static void main(String[] args) {
    for (int i = MAX_STEPS - 1; i >= 0; i--) {
      ProcessStep ps = new ProcessStep(i);
      new Thread(ps).start();
    }
  }
}

signal()メソッドを使用しているが、条件述語が特定のConditionオブジェクトに対応したスレッドだけが復帰する。

信頼できないコードがこのクラスのインスタンスでスレッドを作成することができない場合に限り、この適合コードは安全である。

リスク評価

待機するすべてのスレッドではなく、1つのスレッドだけに通知を行うと、システムの活性が脅かされる恐れがある。

ルール 深刻度 可能性 修正コスト 優先度 レベル
THI02-J P2 L3
関連ガイドライン
CERT C Secure Coding Standard CON38-C. Notify all threads waiting on a condition variable instead of a single thread
参考文献
[API 2006] java.util.concurrent.locks.Condition interface
[JLS 2005] Chapter 17, Threads and Locks
[Goetz 2006] Section 14.2.4, Notification
[Bloch 2001] Item 50. Never invoke wait outside a loop
翻訳元

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

THI02-J. Notify all waiting threads rather than a single thread (revision 117)

Top へ

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