読者です 読者をやめる 読者になる 読者になる

【Effective Java】項目69:wait と notify よりコンカレンシーユティリティを選ぶ(前半)

Java Effective Java

Java のスレッド操作 API として Object.wait() と Object.notify() があります。 しかし、Java 1.5 からはより高いレベルのユティリティが導入されたので wait() と notify() は使うべきではありません。

追加されたのは java.util.concurrent ユティリティです。 concurrent ユティリティは次の3つに分類されます。

エグゼキューターフレームワークについてはすでに項目68で簡単に説明しました。

今回はコンカレントコレクションとシンクロナイザーについて説明します。 また、wait() と notify() を正しく使う方法についても説明します。

コンカレントコレクション

コンカレントコレクションは、List や Map などのコレクションの高パフォーマンスな実装です。 高度な並行性が実装されていて、すべて内部同期されているクラスです。

内部的に同期されているため、これらのクラスから並行性を除去することはできません。 また、外部から追加でロックをすることに意味はありません。 単にプログラムを遅くするだけです。

ConcurrentHashMap

コンカレントコレクションの複数のメソッド呼び出しを外部同期によってアトミックにすることはできません。 代わりに一部のコレクションインタフェースは、アトミックな単一メソッドを提供しています。

たとえば、ConcurrentMap には「指定されたキーをチェックする操作」と「指定されたキーに値を紐付ける操作」をアトミックに行う putIfAbsent() メソッドがあります。

Java 1.5 以前には同期化されたマップの実装として SynchronizedMap がありました。 Java 1.5 以降は、基本的に SynchronizedMap ではなく ConcurrentHashMap を利用するべきです。 ConcurrentHashMap の高度な並行性実装によって、単に SynchronizedMap を ConcurrentHashMap に置き換えるだけで劇的にパフォーマンスが改善する可能性があります。

より一般的には、外部同期されたクラスよりも内部同期された適切なクラスを使うべきです。

ブロック操作

コンカレンシーユティリティのいくつかのインタフェースは操作が完了するまでスレッドを待機させるブロック操作を持っています。

たとえば、BlockingQueue は Queue を拡張しており、take メソッドが追加されています。 このメソッドは「キューの先頭から値を取り出す。ただし、空であれば待機する」という処理を行います。

このクラスはスレッドプールのワークキュー実装に最適です。 実際、エグゼキューターフレームワークのほとんどがこの BlockingQueue を利用しています。

シンクロナイザー

シンクロナイザーは「他のスレッドの完了を待つ」といった処理を可能にするクラスです。 より一般的にはスレッド間の活動を協調するために利用します。

代表的なシンクロナイザーは CountDonwLatch と Semaphore です。 Java 1.5 より前では wait() と notify() を使ってスレッド間の協調を行っていました。 現代においてそれは正しい選択肢ではありません。

CountDownLatch は複数のスレッドの処理が終わることを待ち合わせるために利用できます。 例えばいくつかのタスクの完了を待ち合わせる処理を CountDownLatch を使って実装すると次のようになります。

public static void run(Executor executor, int taskNum) throws InterruptedException {
    // タスク数と同一のカウントを持つラッチを作成
    final CountDownLatch latch = new CountDownLatch(taskNum);
    for (int i = 0; i < taskNum; i++) {
        final int n = i;
        executor.execute(new Runnable() {
            @Override public void run() {
                System.out.println("task#"+ n);
                // ラッチを一つさげる
                latch.countDown();
            }
        });
    }
    // すべてのタスクが終了するのを待つ
    latch.await();
}

実はこの処理を正しく wait() と notify() で実装するのはかなり難しいです。

次回は wait() と notify() の正しい利用方法について説明します。