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

LCK09-J. 途中で待機状態になる可能性のある操作をロックを保持したまま実行しない

ロックを保持したまま、時間が掛かる処理や途中で待機状態になる可能性のある操作を実行すると、深刻なパフォーマンス低下やリソース飢餓状態を引き起こす可能性がある。さらに、相互に依存関係にあるスレッドが無期限に待機状態になるような場合、デッドロックが生じることもありうる。待機状態になる可能性のある操作には、ネットワーク、ファイル、およびコンソールなどの入出力(たとえばConsole.readLine()メソッド)、オブジェクトのシリアライズ、スレッドの無期限な実行の遅延などが含まれる。ロックを保持したプログラムで、待機状態になる可能性のある操作を実行してはいけない。

JVMが信頼性の低いネットワークを通じてファイルシステムとやり取りする場合、ファイル入出力の性能低下が深刻な問題となる可能性がある。このような状況では、ロックを保持したままネットワーク経由でファイル入出力を行うことは回避すべきである。出力用ストリームのロック獲得や入出力操作の完了待ちが必要となるログ出力のようなファイル操作は、専用のスレッドにおいて行うことで処理速度を向上させることができる。ログ出力のリクエストは、リクエストキューに追加するという形にするとよい。ここで、キューへの put() 操作は、ファイル入出力と比較してオーバーヘッドが小さいという想定である[Goetz 2006]。

違反コード (待機スレッド)

以下の違反コード例では、引数timeを受け取るユーティリティメソッドを定義している。

public synchronized void doSomething(long time)
                         throws InterruptedException {
  // ...
  Thread.sleep(time);
}

このメソッドは synchronized 宣言されているため、このメソッドを実行しているスレッドが待機状態になると、他のスレッドはこのクラスの synchronized メソッドを使用できなくなる。Thread.sleep()メソッドでは固有ロックを解放しないからだ。

適合コード (固有ロック)

以下の適合コードでは、引数としてtimeではなくtimeoutを取るdoSomething()メソッドを定義している。Thread.sleep()メソッドの代わりにObject.wait()メソッドを使用することで、待機状態から復帰するまでのタイムアウト値をセットでき、また、タイムアウトする前に他のスレッドからの通知があれば待機状態から復帰できる。

public synchronized void doSomething(long timeout)
                                     throws InterruptedException {
// ...
  while (<条件が一致しない場合>) {
    wait(timeout); // 処理中のモニタを直ちに解放する
  }
}

このコードでは Object.wait() メソッドを利用しているため、処理中のオブジェクトの固有ロックは、待ち状態に移行すると直ちに解放される。タイムアウト後、スレッドはオブジェクトモニタを再度獲得した後に実行を再開する。

Java APIクラス Object のドキュメントによれば [API 2006]

現在のスレッドをこのオブジェクトの待機セットに入れるときに、wait メソッドはこのオブジェクトのロックだけを解除する。現在のスレッドが同期をとる可能性のあるその他のオブジェクトは、このスレッドが待機している間もロックされたままである。このメソッドを呼び出すのは、このオブジェクトのモニタを所有するスレッドだけでなければならない。

他のオブジェクトへのロックを保持するスレッドが待ち状態へ移行する場合は、これらのロックを適切に解放しなければならない。待機と通知に関する補足的なガイドラインについては、「THI03-J. wait() および await() メソッドは常にループ内部で呼び出す」および 「THI02-J. 1つではなくすべての待ち状態スレッドへ通知を行う」に記述されている。

違反コード (ネットワーク入出力)

以下の違反コード例では、サーバからクライアントにPageオブジェクトを送るsendPage()メソッドを定義している。複数のスレッドが同時にアクセスを要求してきた場合に配列 pageBuff を保護するため、メソッドを同期している。

// Page クラスは別途定義されているものとする.
// Page クラスはPage名を保持しており getName() メソッドで取り出せる.
Page[] pageBuff = new Page[MAX_PAGE_SIZE];

public synchronized boolean sendPage(Socket socket, String pageName) 
                                     throws IOException {
  // Page書込み用の出力ストリームを取得する
  ObjectOutputStream out 
      = new ObjectOutputStream(socket.getOutputStream());

  // クライアントから要求されたPageを検索する
  // (この操作は同期が必要)
  Page targetPage = null;
  for (Page p : pageBuff) {
    if (p.getName().compareTo(pageName) == 0) {
      targetPage = p;
    }
  }

  // 要求されたPageが存在しない場合
  if (targetPage == null) {
    return false;
  }

  // クライアントにPageを送信する
  // (同期は不要)
  out.writeObject(targetPage);

  out.flush();
  out.close();
  return true;
}

遅延が大きいネットワークやデータの欠損が頻繁に起こるようなネットワークでは、同期された sendPage() メソッドのなかの writeObject() メソッドを呼び出すと、遅延やデッドロックのように見える状態につながるおそれがある。

適合コード

以下の適合コードでは、一連のプロセスを複数のステップに分割している。

  1. 同期を必要とするデータ構造への操作を行う。
  2. 送信するオブジェクトのコピーを作成する。
  3. 同期しない別メソッドでネットワークを使った通信を行う。

以下の適合コードでは、同期しない sendPage() メソッドから同期メソッド getPage() を呼び出して、要求された PagepageBuff 配列から取得する。Page の取得後、sendPage() メソッドは、同期しない deliverPage() メソッドを呼び出して、クライアントに Page を送信する。

// 同期しない
public boolean sendPage(Socket socket, String pageName) {
  Page targetPage = getPage(pageName);

  if (targetPage == null){
    return false;
  }
  return deliverPage(socket, targetPage);
}

// 同期が必要
private synchronized Page getPage(String pageName) {
  Page targetPage = null;

  for (Page p : pageBuff) {
    if (p.getName().equals(pageName)) {
      targetPage = p;
    }
  }
  return targetPage;
}

// エラー発生時にはfalseを返し、成功時にはtrueを返す
public boolean deliverPage(Socket socket, Page page) {
  ObjectOutputStream out = null;
  boolean result = true;
  try {
    // Page書込み用の出力ストリームを取得する
    out = new ObjectOutputStream(socket.getOutputStream());

    // クライアントにPageを送信する
    out.writeObject(page);out.flush();
  } catch (IOException io) {
    result = false;
  } finally {
    if (out != null) {
      try {
        out.close();
      } catch (IOException e) {
        result = false;
      }
    }
  }
  return result;
}
例外

LCK09-EX0: 呼出し元に適切な終了メカニズムを提供するクラスは、このガイドラインに従わなくてもよい。詳細は、「THI04-J. ブロックしているスレッドやタスクが確実に終了できるようにする」を参照。

LCK09-EX1: 複数のロックを必要とするメソッドは、いくつかのロックを保持した状態で、残りの必要なロックが利用可能になるのを待つ場合がある。このような場合は、このガイドラインに従わなくてもよい。ただし、他の適用可能なガイドラインには従わなければならない。とくに、デッドロックを回避するために「LCK07-J. デッドロックを回避するためにロックは同一順序で要求および解放する」に適合すること。

リスク評価

同期ブロック内で行われる処理が待ち状態になったり、処理完了までに長い時間がかかる場合、デッドロックが発生したりシステムが無反応になってしまうおそれがある。

ルール 深刻度 可能性 修正コスト 優先度 レベル
LCK09-J P2 L3
関連ガイドライン
CERT C Secure Coding Standard CON36-C. Do not perform operations that can block while holding a lock
参考文献
[API 2006] Class Object
[Grosso 2001] Chapter 10, Serialization
[JLS 2005] Chapter 17, Threads and Locks
[Rotem 2008] Fallacies of Distributed Computing Explained
翻訳元

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

LCK09-J. Do not perform operations that can block while holding a lock (revision 110)

Top へ

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