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

LCK11-J. 一貫したロック方式を定めていないクラスを使用する場合、クライアントサイドロックを行わない

Goetz は以下のように述べている。[Goetz 2006]

クライアントサイドロックとは、オブジェクトXを使うクライアントコードを、Xが自分のステートをガードするために使っているロックでガードすることである。クライアントサイドロックを使うためには、Xが使っているロックを知る必要がある。

クライアントサイドロックは、スレッドセーフなクラスが、一定のロック方式を用いることを宣言し、かつそれを明文化している場合には使ってもよいが、Goetz はそれが誤用されることを警告している。[Goetz 2006]

クラスを拡張してアトミックな操作を新たに加えるやり方は、ロック関連のコードを複数のクラスに分散させるので脆弱であるが、クライアントサイドロックはそれに輪をかけて脆弱である。クラスCのロック関連のコードを、Cとまったく関係のないクラスに置くからである。クラスのロックの実装に関与しないクライアントサイドロックを使うときは、注意が必要である(クラス側のロックの方式が変わるかもしれないので)。

クライアントサイドロックをサポートするクラスは、その適切な用法を明示的に文書化するべきである。たとえば、java.util.concurrent.ConcurrentHashMap<K,V>クラスは、クライアントサイドロックに使用するべきでない。 Java API 仕様には以下のように記されている。[API 2006]

ただし、すべての操作がスレッドセーフである場合でも、取得操作にロックは含まれないため、テーブル全体がロックされてすべてのアクセスが拒否されることはない。このクラスは、スレッドの安全性には依存するが、同期の詳細に依存しないプログラム内で Hashtable との完全な相互運用が可能である。

クライアントサイドロックの適用は、使用するクラスのドキュメントで推奨されている場合のみに限定するのが望ましい。たとえば、java.util.Collections クラスの synchronizedList() ラッパーメソッドについて、Java API 仕様には以下のように記されている。[API 2006]

確実に直列アクセスを実現するには、基になるリストへのアクセスはすべて、返されたリストを介して行う必要がある。返されたリストの繰り返し処理を行う場合、ユーザは、次に示すように手動で同期をとる必要がある。これを行わない場合、動作は保証されない。

なお、基になるリストが信頼できないクライアントからアクセスできない場合、上記の内容は「LCK04-J. 基になるコレクションにアクセス可能な場合にはコレクションビューを使って同期しない」に適合している。

違反コード (固有ロック)

この違反コードでは、スレッドセーフなBookクラスを使用しているが、このクラスは直接修正できない。クラスを直接修正できない理由としては、たとえば、ソースコードをレビュー用に入手できないか、クラスが拡張できない汎用ライブラリの一部である場合などが考えられる。

final class Book {
  // 将来、private final宣言したロックを使用するように
  // ロックポリシーを変更するかもしれない
  private final String title;
  private Calendar dateIssued;
  private Calendar dateDue;

  Book(String title) {
    this.title = title;
  }

  public synchronized void issue(int days) {
    dateIssued = Calendar.getInstance();
    dateDue = Calendar.getInstance();
    dateDue.add(dateIssued.DATE, days);
  }

  public synchronized Calendar getDueDate() {
    return dateDue;
  }
}

上記のクラスは、一貫して特定のロック方式を利用するとは明言していない(つまり、ロック方式は予告なしに変更される可能性がある)。さらに、呼出し元がクライアントサイドロックを安全に使用できるとも明示されていない。BookWrapper クライアントクラスは、renew() メソッド内で Book インスタンスを用いた同期によるクライアントサイドロックを使用している。

// クライアント側
public class BookWrapper {
  private final Book book;

  BookWrapper(Book book) {
    this.book = book;
  }

  public void issue(int days) {
    book.issue(days);
  }

  public Calendar getDueDate() {
    return book.getDueDate();
  }

  public void renew() {
    synchronized(book) {
      if (book.getDueDate().before(Calendar.getInstance())) {
        throw new IllegalStateException("Book overdue");
      } else {
        book.issue(14); // 14日間貸し出しを延長
      }
    }
  }
}

Bookクラスの同期ポリシーが将来変更された場合、BookWrapperクラスで実装されているロック方式は不適切になるかもしれない。たとえば、Bookクラスが private final 宣言したロックオブジェクトを使用するように変更された場合(「LCK00-J. 信頼できないコードから使用されるクラスを同期するにはprivate finalロックオブジェクトを使用する」で推奨されているように)、BookWrapperクラスのロック方式は破綻する。これは、BookWrapper.getDueDate()メソッドを呼び出すスレッドが、新たなロックポリシーに基づいてスレッドセーフなBook クラスにおける処理を行うからである。しかし、renew()メソッドを呼び出すスレッドは常にBookインスタンスの固有ロックを使って同期することになる。つまり、実装上2つの異なるロックが使用されることになる。

適合コード (private final ロックオブジェクト)

以下の適合コードでは、private final宣言されたロックオブジェクトを使用してBookWrapperクラスのメソッドを同期している。

public final class BookWrapper {
  private final Book book;
  private final Object lock = new Object();

  BookWrapper(Book book) {
    this.book = book;
  }

  public void issue(int days) {
    synchronized(lock) {
      book.issue(days);
    }
  }

  public Calendar getDueDate() {
    synchronized(lock) {
      return book.getDueDate();
    }
  }

  public void renew() {
    synchronized(lock) {
      if (book.getDueDate().before(Calendar.getInstance())) {
        throw new IllegalStateException("Book overdue");
      } else {
        book.issue(14); // 14日間貸し出しを延長
      }
    }
  }
}

適合コードにおけるBookWrapperクラスのロック方式は、Bookインスタンスのロックポリシーに依存していない。

違反コード (クラス継承とアクセス可能なメンバのロック)

スレッドセーフなクラスに機能を追加するために行うクラス拡張の脆さについて、Goetzは以下のように記述している [Goetz 2006]。

拡張によるサブクラス作りはコードを直接クラスに加える方法に比べてやや不安定である。同期化ポリシーの実装が複数の、しかも統一的にメンテナンスされるとはかぎらないソースファイルに分散するからである。元のクラスが同期化ポリシーを変更して、ステート変数をガードするために別のロックを選んだら、サブクラスは、自分は何もしていないのに分かりづらいバグを抱えるであろう。ベースクラスのステートへの並行アクセスを制御するために使うロックが、正しいロックでなくなるからである。

以下の違反コードでは、PrintableIPAddressListクラスはスレッドセーフなIPAddressListクラスを拡張している。PrintableIPAddressListは、addAndPrintIPAddresses()メソッド内でIPAddressList.ipsをロックしている。このコードは、スーパークラスが所有しロックしているオブジェクトをサブクラスが使用するクライアントサイドロックの一例である。

// このクラスは、将来的にはそのロックポリシーを変更するかもしれない。
// たとえば、新規のアトミックでないメソッドを追加する場合
class IPAddressList {
  private final List<InetAddress> ips = 
      Collections.synchronizedList(new ArrayList<InetAddress>());

  public List<InetAddress> getList() {
    return ips; // パッケージプライベートな可視性のため
                // 防御的なコピーは必要ではない
  }

  public void addIPAddress(InetAddress address) {
    ips.add(address);
  }
}

class PrintableIPAddressList extends IPAddressList {
  public void addAndPrintIPAddresses(InetAddress address) {
    synchronized (getList()) {
      addIPAddress(address);
      InetAddress[] ia =
          (InetAddress[]) getList().toArray(new InetAddress[0]);
      // ...
    }
  }
}

LCK00-J. 信頼できないコードから使用されるクラスを同期するにはprivate finalロックオブジェクトを使用する」が推奨するように、IPAddressListクラスを、private final宣言したロックオブジェクトを用いてブロック同期を行うように変更した場合、PrintableIPAddressListサブクラス自身に変更はなくても、ロック方式は破綻する。また、Collections.synchronizedList()のようなラッパーが使用される場合、クラス拡張を行うためにラップされたクラスをクライアントで判定することは難しくなるであろう。[Goetz 2006]

適合コード (コンポジション)

以下の適合コードでは、IPAddressListクラスのオブジェクトをラップし、オブジェクトの状態を操作するための同期されたメソッドを提供している。

コンポジションはカプセル化の利点を最小限のオーバーヘッドで提供する。コンポジションについて詳しくは「OBJ02-J. スーパークラスに変更を加える場合、サブクラスの依存性を保つ」を参照のこと。

// Class IPAddressList への変更はない
class PrintableIPAddressList {
  private final IPAddressList ips;

  public PrintableIPAddressList(IPAddressList list) {
    this.ips = list;
  }

  public synchronized void addIPAddress(InetAddress address) {
    ips.addIPAddress(address);
  }

  public synchronized void addAndPrintIPAddresses(InetAddress address) {
    addIPAddress(address);
    InetAddress[] ia =
        (InetAddress[]) ips.getList().toArray(new InetAddress[0]);
    // ...
  }
}

この場合、コンポジションにより、PrintableIPAddressListクラスが、基となるリストクラスのロックとは無関係にそれ自身の固有ロックを使用することを可能にしている。PrintableIPAddressListラッパーが、同期用のロックオブジェクトとして自身を使用することにより、基となるコレクションのメソッドへの直接アクセスを防いでいるため、元になるコレクションがスレッドセーフである必要はない。このアプローチでは、基となるクラスが将来ロックポリシーを変更したとしても、一貫性あるロックを提供できる [Goetz 2006]。

リスク評価

スレッドセーフなクラスのロック方式が一貫していない場合にクライアントサイドロックを使用すると、データの不整合やデッドロックが発生する恐れがある。

ルール 深刻度 可能性 修正コスト 優先度 レベル
LCK11-J P4 L3
参考文献
[API 2006] Class Vector, Class WeakReference, Class ConcurrentHashMap<K,V>
[JavaThreads 2004] 8.2, Synchronization and Collection Classes
[Goetz 2006] 4.4.1, Client-side Locking; 4.4.2, Composition; and 5.2.1, ConcurrentHashMap
[Lee 2009] Map & Compound Operation
翻訳元

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

LCK11-J. Avoid client-side locking when using classes that do not commit to their locking strategy (revision 82)

Top へ

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