Javaのwaitおよびnotify()メソッド

Javaのwaitおよびnotify()メソッド

1. 前書き

この記事では、Javaの最も基本的なメカニズムの1つであるスレッド同期について説明します。

最初に、いくつかの重要な並行性関連の用語と方法論について説明します。

そして、wait()notify().をよりよく理解することを目的として、並行性の問題に対処する単純なアプリケーションを開発します。

2. Javaでのスレッド同期

マルチスレッド環境では、複数のスレッドが同じリソースを変更しようとする場合があります。 スレッドが適切に管理されていない場合、もちろん、これは一貫性の問題につながります。

2.1. Javaの保護されたブロック

Javaの複数のスレッドのアクションを調整するために使用できるツールの1つに、保護されたブロックがあります。 このようなブロックは、実行を再開する前に特定の条件のチェックを保持します。

それを念頭に置いて、以下を利用します。

これは、Threadのライフサイクルを示す次の図からよりよく理解できます。

image

このライフサイクルを制御する方法はたくさんあることに注意してください。ただし、この記事では、wait()notify().のみに焦点を当てます。

3. wait()メソッド

簡単に言えば、wait() –を呼び出すと、他のスレッドが同じオブジェクトでnotify()またはnotifyAll()を呼び出すまで、現在のスレッドを強制的に待機させます。

このため、現在のスレッドがオブジェクトのモニターを所有している必要があります。 Javadocsによると、これは次の場合に発生する可能性があります。

  • 指定されたオブジェクトに対してsynchronizedインスタンスメソッドを実行しました

  • 指定されたオブジェクトに対してsynchronizedブロックの本体を実行しました

  • タイプClassのオブジェクトに対してsynchronized staticメソッドを実行する

一度に1つのアクティブなスレッドのみがオブジェクトのモニターを所有できることに注意してください。

このwait()メソッドには、3つのオーバーロードされたシグネチャが付属しています。 これらを見てみましょう。

3.1. wait()

wait()メソッドにより、現在のスレッドは、別のスレッドがこのオブジェクトのnotify()またはnotifyAll()を呼び出すまで無期限に待機します。

3.2. wait(long timeout)

このメソッドを使用して、スレッドが自動的に起動されるまでのタイムアウトを指定できます。 スレッドは、notify()またはnotifyAll().を使用して、タイムアウトに達する前にウェイクアップできます。

wait(0)を呼び出すことは、wait().を呼び出すことと同じであることに注意してください。

3.3. wait(long timeout, int nanos)

これは、同じ機能を提供するもう1つのシグネチャですが、唯一の違いは、より高い精度を提供できることです。

合計タイムアウト期間(ナノ秒単位)は、1_000_000*timeout + nanos.として計算されます

4. notify()およびnotifyAll()

notify()メソッドは、このオブジェクトのモニターへのアクセスを待機しているスレッドをウェイクアップするために使用されます。

待機中のスレッドに通知する方法は2つあります。

4.1. notify()

wait()メソッドのいずれかを使用して)このオブジェクトのモニターで待機しているすべてのスレッドについて、メソッドnotify()はそれらのいずれかに任意にウェイクアップするように通知します。 起動するスレッドの正確な選択は非決定的であり、実装に依存します。

notify()は単一のランダムスレッドをウェイクアップするため、スレッドが同様のタスクを実行する相互排他ロックを実装するために使用できますが、ほとんどの場合、notifyAll()を実装する方が実行可能です。

4.2. notifyAll()

このメソッドは、このオブジェクトのモニターで待機しているすべてのスレッドを単にウェイクアップします。

起動されたスレッドは、他のスレッドと同様に通常の方法で完了します。

ただし、実行の続行を許可する前に、常にdefine a quick check for the condition required to proceed with the thread –通知を受信せずにスレッドがウェイクアップする場合があるためです(このシナリオについては、後の例で説明します)。

5. 送信者と受信者の同期の問題

基本を理解したところで、wait()メソッドとnotify()メソッドを使用してそれらの間の同期を設定する単純なSenderReceiverアプリケーションを見ていきましょう。

  • Senderは、データパケットをReceiverに送信することになっています。

  • Senderは、データパケットの送信が完了するまで、データパケットを処理できません。

  • 同様に、Receiverが前のパケットをすでに処理していない限り、Senderは別のパケットを送信しようとしてはなりません。

まず、SenderからReceiver.に送信されるデータpacketで構成されるDataクラスを作成しましょう。wait()notifyAll()を使用します。それらの間の同期を設定するには:

public class Data {
    private String packet;

    // True if receiver should wait
    // False if sender should wait
    private boolean transfer = true;

    public synchronized void send(String packet) {
        while (!transfer) {
            try {
                wait();
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
        transfer = false;

        this.packet = packet;
        notifyAll();
    }

    public synchronized String receive() {
        while (transfer) {
            try {
                wait();
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
        transfer = true;

        notifyAll();
        return packet;
    }
}

ここで何が起こっているのかを分析してみましょう。

  • packet変数は、ネットワークを介して転送されているデータを示します

  • SenderReceiverが同期に使用するboolean変数transfer –があります。

    • この変数がtrueの場合、ReceiverSenderがメッセージを送信するのを待つ必要があります

    • falseの場合、SenderReceiverがメッセージを受信するのを待つ必要があります

  • Senderは、send()メソッドを使用してデータをReceiverに送信します。

    • transferfalse,の場合、このスレッドでwait()を呼び出して待機します

    • ただし、trueの場合は、ステータスを切り替え、メッセージを設定し、notifyAll()を呼び出して他のスレッドをウェイクアップし、重要なイベントが発生したことを指定して、実行を続行できるかどうかを確認できます。

  • 同様に、Receiverreceive()メソッドを使用します。

    • transferSenderによってfalseに設定されている場合は、それだけが続行されます。それ以外の場合は、このスレッドでwait()を呼び出します。

    • 条件が満たされると、ステータスを切り替え、待機中のすべてのスレッドにウェイクアップを通知し、Receiverであったデータパケットを返します。

5.1. whileループでwait()を囲むのはなぜですか?

notify()notifyAll()は、このオブジェクトのモニターで待機しているスレッドをランダムにウェイクアップするため、条件が満たされていることが常に重要であるとは限りません。 スレッドがウェイクアップされることがありますが、実際にはまだ条件が満たされていません。

また、偽のウェイクアップ(スレッドが通知を受信せずに待機状態からウェイクアップできる)から私たちを救うチェックを定義することもできます。

5.2. send()メソッドとreceive()メソッドを同期する必要があるのはなぜですか?

これらのメソッドをsynchronizedメソッド内に配置して、組み込みロックを提供しました。 wait()メソッドを呼び出すスレッドが固有のロックを所有していない場合、エラーがスローされます。

次に、SenderReceiverを作成し、両方にRunnableインターフェースを実装して、それらのインスタンスをスレッドで実行できるようにします。

まず、Senderがどのように機能するかを見てみましょう。

public class Sender implements Runnable {
    private Data data;

    // standard constructors

    public void run() {
        String packets[] = {
          "First packet",
          "Second packet",
          "Third packet",
          "Fourth packet",
          "End"
        };

        for (String packet : packets) {
            data.send(packet);

            // Thread.sleep() to mimic heavy server-side processing
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
    }
}

このSenderの場合:

  • packets[]配列でネットワークを介して送信されるいくつかのランダムデータパケットを作成しています

  • パケットごとに、単にsend()を呼び出しています

  • 次に、サーバー側の重い処理を模倣するために、ランダムな間隔でThread.sleep()を呼び出します

最後に、Receiverを実装しましょう。

public class Receiver implements Runnable {
    private Data load;

    // standard constructors

    public void run() {
        for(String receivedMessage = load.receive();
          !"End".equals(receivedMessage);
          receivedMessage = load.receive()) {

            System.out.println(receivedMessage);

            // ...
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
    }
}

ここでは、最後の“End”データパケットを取得するまで、ループ内でload.receive()を呼び出すだけです。

このアプリケーションの動作を見てみましょう。

public static void main(String[] args) {
    Data data = new Data();
    Thread sender = new Thread(new Sender(data));
    Thread receiver = new Thread(new Receiver(data));

    sender.start();
    receiver.start();
}

次の出力が表示されます。

First packet
Second packet
Third packet
Fourth packet

そして、ここに–we’ve received all data packets in the right, sequential orderがあり、送信者と受信者の間の正しい通信を正常に確立しました。

6. 結論

この記事では、Javaのコア同期の概念について説明しました。具体的には、wait()notify()を使用して興味深い同期の問題を解決する方法に焦点を当てました。 そして最後に、これらの概念を実際に適用するコードサンプルを試しました。

ここで説明する前に、wait()notify()notifyAll()などのこれらすべての低レベルAPIは、適切に機能する従来のメソッドですが、高レベルのメカニズムは多くの場合、JavaのネイティブLockおよびConditionインターフェイス(java.util.concurrent.locksパッケージで利用可能)など、よりシンプルで優れています。

java.util.concurrentパッケージの詳細については、overview of the java.util.concurrentの記事を参照してください。LockConditionguide to java.util.concurrent.Locks, hereで説明されています。

いつものように、この記事で使用されている完全なコードスニペットは利用可能ですover on GitHub.