Previous | Next | Trail Map | Writing Java Programs | スレッドの制御


スレッドの同期

これまでのところ、このレッスンでは独立して非同期のスレッドの例を説明した。 つまり、それぞれのスレッドがデータのすべてを含んでいて、メソッドは実行のために必要とされ、外部の資源あるいはメソッドを必要としていなかった。 さらに、それらの例のスレッドは、同時にスレッドを走らせている他の状態あるいは活動には関わりなく、独自のペースで実行した。

しかし、別々の同時に実行しているスレッドがデータを共有し、互いのスレッドの状態と活動を考慮しなければならない興味深い状況もたくさんある。 このようなプログラミングの状況の1つは、プロデューサ/コンシューマのシナリオとして知られている。この場合、プロデューサはコンシューマが消費するデータのストリームを作成する。

例えば、1つのスレッド(プロデューサ)がファイルにデータを書き込み、 2 番目のスレッド(コンシューマ)が同じファイルからデータを読み込む Java アプリケーションをイメージすることができる。 あるいは、キーボードで文字を入力するときに、プロデューサスレッドはイベント待ち行列に重要なイベントを配置し、コンシューマスレッドは同じ待ち行列からイベントを読み込む。 これらの例は両方とも、ファイル、イベント待ち行列など共通の資源を共有する並行スレッドを使用する。 そしてスレッドが共通の資源を共有するので、それらはいくつかの方法で同期をとらなくてはならない。

このレッスンでは、簡単なプロデューサ/コンシューマの例を通して、Java スレッドの同期について説明する。

プロデューサ/コンシューマの例

Producer は、 0 から9までの整数を作成して、それを「CubbyHole(CubbyHole)」オブジェクトに格納し、作成した数を出力する。(同期問題をいっそう面白くするために)プロデューサは、 0 から 100 ミリ秒の間をランダムな時間スリープする。

class Producer extends Thread {
    private CubbyHole cubbyhole;
    private int number;

    public Producer(CubbyHole c, int number) {
        cubbyhole = c;
        this.number = number;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            cubbyhole.put(i);
            System.out.println("Producer #" + this.number + " put: " + i);
            try {
                sleep((int)(Math.random() * 100));
            } catch (InterruptedException e) {
            }
        }
    }
}

貪欲な Consumer は、整数が有効になるとすぐに CubbyHole (Producer が最初に整数を置いたのとまったく同じオブジェクト)からすべての整数を消費する。

class Consumer extends Thread {
    private CubbyHole cubbyhole;
    private int number;

    public Consumer(CubbyHole c, int number) {
        cubbyhole = c;
        this.number = number;
    }

    public void run() {
        int value = 0;
        for (int i = 0; i < 10; i++) {
            value = cubbyhole.get();
            System.out.println("Consumer #" + this.number + " got: " + value);
        }
    }
}

この例でのプロデューサとコンシューマは、共通の CubbyHole オブジェクトを通してデータを共有する。 プロデューサとコンシューマのいずれも、一回および一度だけ生産された値をコンシューマが確実に獲得するために全く努力していないことが、注目される。 これら2つのスレッド間の同期は、CubbyHoleオブジェクトの get() put()メソッド内の低いレベルにおいて実際に発生する。 しかし、これらの2つのスレッドが同期のために配列を作らないと仮定して、その状況で起こる可能性のある問題について話をしよう。

プロデューサがコンシューマより速くて、コンシューマが最初の数を消費する機会を得る前に2つの数を作成すると、1つ問題が発生する。 したがってコンシューマは数をスキップする。 出力の一部が次のようになるかもしれない。

. . .

Consumer #1 got: 3
Producer #1 put: 4
Producer #1 put: 5
Consumer #1 got: 5

もう1つの問題は、コンシューマがプロデューサより速くて、2度同じ値を消費する時に発生する可能性がある。 この状況では、コンシューマは2度同じ値を出力して、次のような出力になるかもしれない。

. . .

Producer #1 put: 4
Consumer #1 got: 4
Consumer #1 got: 4
Producer #1 put: 5

いずれの方法でも、結果は誤っている。 コンシューマに求められるのは、プロデューサが正確に一度作り出したどの整数も得ることである。 今述べたような、同時に一つのオブジェクトにアクセスしようとして誤った結果になるという、複数の非同期に実行するスレッドから発生する問題は、レース状態と呼ばれる。

このプロデューサ/コンシューマの例でレース状態を防ぐためには、プロデューサによる CubbyHole への新規の整数の貯蔵と、コンシューマによる CubbyHole からの整数の検索とで、同期をとらなくてはならない。 コンシューマは正確に一度それぞれの整数を消費しなくてはならない。 プロデューサ/コンシューマのプログラムは、プロデューサスレッドとコンシューマスレッドの同期をとるために、モニタ、および notify() wait() メソッドの2つの異なるメカニズムを使用する。

モニタ

2つのスレッド間で共用され、そのアクセスが同期をとらなくてはならない CubbyHoleのようなオブジェクトは、条件変数と呼ばれる。 Java 言語では、モニタの使用によって条件変数の周囲のスレッドの同期をとることができる。 モニタは、2つのスレッドが同時に同じ変数にアクセスするのを防止する。

notify() と wait() メソッド

より高いレベルにおいて、プロデューサ/コンシューマの例は、オブジェクトの notify() wait() メソッドを使用して、それらの活動を調整する。 プロデューサとコンシューマは、プロデューサがCubbyHoleに配置する値が、コンシューマによって一回および一度だけ検索されるのを保証するために、 notify() wait() を使用する。

メインプログラム

次は CubbyHole オブジェクト、プロデューサ、コンシューマを作成して、プロデューサとコンシューマ両方を開始する小さなスタンドアロンの Java アプリケーションである。

class ProducerConsumerTest {
    public static void main(String args[]) {
        CubbyHole c = new CubbyHole();
        Producer p1 = new Producer(c, 1);
        Consumer c1 = new Consumer(c, 1);

        p1.start();
        c1.start();
    }
}

出力

次は ProducerConsumerTest の出力である。

Producer #1 put: 0
Consumer #1 got: 0
Producer #1 put: 1
Consumer #1 got: 1
Producer #1 put: 2
Consumer #1 got: 2
Producer #1 put: 3
Consumer #1 got: 3
Producer #1 put: 4
Consumer #1 got: 4
Producer #1 put: 5
Consumer #1 got: 5
Producer #1 put: 6
Consumer #1 got: 6
Producer #1 put: 7
Consumer #1 got: 7
Producer #1 put: 8
Consumer #1 got: 8
Producer #1 put: 9
Consumer #1 got: 9

試してみよう: 上記で示した CubbyHole クラスのリストで太字で示される行を削除する。 プログラムを再コンパイルして、再度実行しなさい。 何が起きたか?プロデューサとコンシューマのスレッドの同期をとる明示的な努力をしていないので、コンシューマは無茶な消費をし、正確に一度づつ 0から9までの各整数を得る代わりに、すべてゼロの集合を得る。


Previous | Next | Trail Map | Writing Java Programs | スレッドの制御