The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目70:スレッド安全性を文書化する

開発者は、クラスがマルチスレッド環境で利用された場合にどのように振る舞うかを文書化しなければなりません。

マルチスレッド環境における振る舞いを文書化しない場合、クラスの利用者は十分な同期を行わない(項目66)かもしれませんし、逆に過度な同期を行う(項目67)かもしれません。

synchronized 修飾子を手がかりにしてクラスのスレッドセーフ性を確認することは大きな誤りです。 synchronized 修飾子は API の仕様ではなく実装の詳細だからです。

synchronized 修飾子があるからといってスレッドセーフは保証されませんし、synchronized 修飾子がなくてもスレッドセーフ性が保証される場合もあります。

スレッドセーフレベル

スレッドセーフには次のようなレベルがあります。

  • 不変(immutable)
  • 無条件スレッドセーフ(unconditionally thread-safe)
  • 条件付きスレッドセーフ(conditionally thread-safe)
  • スレッドセーフでない(not thread-safe)
  • スレッド敵対(thread-hostile)

不変

スレッドセーフレベルが不変の場合、クラスのインスタンスは不変であるため、外部同期は一切必要ありません。 String, Integer, BigInteger などがその例です。

無条件スレッドセーフ

スレッドセーフレベルが無条件スレッドセーフの場合、クラスは可変です。 しかし、すべてのパブリックメソッドはスレッドセーフであることが保証されます。

クラスは適切に内部同期されているため、外部同期は一切必要ありません。 例としては、ConcurrentHashMap などです。

無条件スレッドセーフを実現したい場合、プライベートロックイディオムを使うことが効果的です。

private final Object lock = new Object();
public void foo() {
    synchronized(lock) {
        ....
    }
}

クライアントがロックオブジェクトに触ることができると、悪意あるクライアントによってサービス拒否攻撃が行われる可能性があります。 このイディオムを用いれば、外部からロックオブジェクトに触ることが出来なくなり、この攻撃を防ぐことができます。

また、プライベートロックイディオムは継承のために設計されたクラスに対して有効です。

条件付きスレッドセーフ

このレベルの場合、クラスは可変であり、いくつかのメソッドは適切な外部同期を必要とします。 例えば Collections.synchronized ラッパーが返すコレクションです。

Collections.synchronized ラッパーが返すコレクションのイテレータを利用したい場合、次のような外部同期が必要です。

Map<K, V> map = Collections.synchronizedMap(new HashMap<K, V>()));
Set<K> set = m.keySet(); // ここでは同期は必要ない
// イテレータを利用する場合、map に対するロックが必要
synchronized (map) {
    for (K key: set) { 
        ....
    }
}

このように、条件付きスレッドセーフレベルではクライアントにロックオブジェクトを渡す必要があります。 そのため、条件付きスレッドセーフではプライベートロックイディオムを使うことはできません。

スレッドセーフでない

このレベルでは、クラスは可変ですべてのメソッドが外部同期が必要です。 例えば、ArrayList や HashMap などです。

スレッド敵対

このレベルのクラス/メソッドは、たとえ外部同期されていてもマルチスレッドで利用することは安全ではありません。

このクラスは Java ライブラリにもほとんどありません。 希有な例としては System.runFinalizersOnExit() です。

感想

System.runFinalizersOnExit() がなぜだめなのか調べた。

まずはリファレンスをチェック。

Runtime (Java Platform SE 7)

非推奨。 このメソッドは本質的に安全ではありません。ファイナライザがライブオブジェクトに対して呼び出される結果になる可能性があり、そのときにほかのスレッドがそれらのオブジェクトを並行して操作していると、動作が異常になるか、デッドロックが発生します。   終了時のファイナライズを有効または無効にします。これを実行することによって、自動的に呼び出されていないファイナライザを持つすべてのオブジェクトのファイナライザが呼び出され、Java Runtime の終了前に実行されるようになります。

なるほど、これをオンにすると終了時に各オブジェクトの finalize() メソッドが必ず呼び出されるようになる。 この時、そのオブジェクトが並行に実行されていたりする可能性があるので危険、ということだな。

(c) The King's Museum