Advanced   Java   Services Die Methoden wait() und notify() aus der Klasse Object


wait() und notify() sind ein Team

Mit wait() geht ein Thread in einen Wartezustand, der im Normalfall durch ein notify() oder notifyAll() beendet wird. In Ausnahmefällen kann ein wait() auch durch eine Interruptaufforderung beendet werden. wait(), notify() und notifyAll() stehen für jedes Objekt zur Verfügung können aber nur in synchronisierten Codeabschnitten aufgerufen werden, andernfalls werfen sie eine IllegalMonitorStateException. Es ist wichtig, daran zu erinnern, daß wait() den Lock zurückgibt.


Beispiel 1: Drei Waiter, ein notify

Das Beispiel zeigt einen Thread, der zu einem Objekt wait() aufruft. wait() blockiert bis ein anderer Thread zum selben Objekt ein notify aufruft.

Der Thread

import java.time.LocalTime;

public class Waiter extends Thread
{
   private Object ob;

   /**
    */
   public Waiter(Object ob)
   {
    this.ob = ob;
   }

   public void run()
   {
    // waiter warten sofort
      synchronized(ob)
      {
         try
         {
            System.out.println(this.getName() + " waits");
            ob.wait();
            System.out.println(this.getName() + " ends waiting");
         }
         catch(InterruptedException ex)
         {
            System.out.println(this.getName() + " " +ex);
         }
      }
   }
}

Die Mainklasse

main legt ein Object an und übergibt es an drei Threads. Nach dem Start wartet main eine Sekunde und weckt mit notify einen der wartenden Threads. Die anderen beiden werden zwei Sekunden später durch Interrupt-Aufrufe beendet. Da sie keine Dämonen sind würde ansonsten main nicht enden.

public class Wait_Notify_1
{
   public static void main(String[] args)
   {
      Object ob = new Object();
      Waiter wt1 = new Waiter(ob);
      Waiter wt2 = new Waiter(ob);
      Waiter wt3 = new Waiter(ob);
      wt1.start();
      wt2.start();
      wt3.start();

      try { TimeUnit.SECONDS.sleep(1); } catch(InterruptedException ex) {}

      synchronized(ob)
      {
        System.out.println("main calls notify");
         ob.notify();
      }
      try { TimeUnit.SECONDS.sleep(2); } catch(InterruptedException ex) {}
    System.out.println("main interrupts all waiting threads");

      wt1.interrupt();
      wt2.interrupt();
      wt3.interrupt();
   }
}

Die Ausgabe

Thread-0 waits
Thread-1 waits
Thread-2 waits
main calls notify
Thread-0 ends waiting
main interrupts all waiting threads
Thread-1 java.lang.InterruptedException
Thread-2 java.lang.InterruptedException

Man beachte, daß wait() und notify() zum gleichen Objekt synchronisiert sind. Das ist notwendig. Eine Synchronistion zu verschiedenen Objekten führt ebenfalls zu einer IllegalMonitorStateException.


Beispiel 2: Drei Waiter, ein notifyAll

Der Waiter ist identisch zum ersten Beispiel.



In der Mainklasse legen wir nun ein Objekt an und wieder drei Threads, die dieses Objekt erhalten. Diesmal aber wird notifyAll gerufen.

public class Wait_Notify_01
{
   public static void main(String[] args)
   {
      Object ob = new Object();
      Waiter w0 = new Waiter(ob);
      Waiter w1 = new Waiter(ob);
      Waiter w2 = new Waiter(ob);
      w0.start();
      w1.start();
      w2.start();

      try { TimeUnit.SECONDS.sleep(2); } catch(InterruptedException ex) {}

      synchronized(ob)
      {
        System.out.println("main calls notifyAll");
         ob.notifyAll();
      }
   }
}

Hier drei mögliche Ausgaben des Programms

Thread-0 waits
Thread-2 waits
Thread-1 waits
main calls notifyAll
Thread-1 ends waiting 15:17:53.271
Thread-2 ends waiting 15:17:53.279
Thread-0 ends waiting 15:17:53.279
Thread-0 waits
Thread-1 waits
Thread-2 waits
main calls notifyAll
Thread-2 ends waiting 15:21:03.761
Thread-1 ends waiting 15:21:03.769
Thread-0 ends waiting 15:21:03.769


Consumer-Producer Modell

Da wait() den Lock zurückgibt eignet es sich gut für sogenante Consumer-Producer Modelle. Consumer und Producer sind Threads die ein Lager leeren oder füllen, d.h. sie greifen auf ein gemeinsames Lager zu. Das Lager bietet put() und get() Methoden an mit denen man Waren im Lager deponieren oder entnehmen kann. put() und get() können mit wait() in einen Wartezustand gehen je nachdem ob das Lager voll oder leer ist. Daher sind put() und get() synchronized.


Beispiel 3

Die klassische Lösung des Consumer-Producer-Modells ist zugleich simpel und raffiniert. Diese Lösung arbeitet mit einer einzigen booleschen Variablen, die meist available heißt. Die put()-Methode "denkt" sich, "solange was da ist, braucht man nichts Auffüllen, also warten", und die get()-Methode denkt sich "solange nichts da ist, kann nichts abgeholt werden, also warten". In den put()- und get()-Methoden wechseln sich also wait() und notify() ab.

In diesem Beispiel leeren (Consumer) oder füllen (Producer) das Lager in unregelmäßigen Abständen quasi endlos. Dies wird durch unterschiedliche sleep()-Zeiten und eine Endlosschleife realisiert.

In main() soll das Programm durch Interrupt-Aufrufe für Consumer und Producer beendet werden. Die Interrupts können die Threads in zwei verschiedenen Zuständen erreichen, in der sleep()-Phase oder in der wait()-Phase. In beiden Fällen soll die Anwendung beendet werden, hierbei fordert die Beendigung durch Unterbrechung der wait()-Phase den größeren Aufwand. Das Beenden während der wait()-Phase wird durch geeignete Returnwerte der get()- und put()-Methode erreicht.

Das Lager sieht daher folgendermaßen aus.

public class Stock
{
   private int MAX = 10; // maximaler Bestand
   private int content; // aktueller Lagerbestand
   private boolean available = false;

   public Stock(int MAX)
   {
      this.MAX = MAX;
      this.content = this.MAX - 1;
   }

   public synchronized int put()
   {
      putCount++;

      // solange was da, braucht nicht aufgefüllt werden, also warten
      while(available == true)
      {
         System.out.println(Thread.currentThread().getName() + " must wait");
         try
         {
            wait();
         }
         catch(InterruptedException ex)
         {
            return -1;
         }
      }
      //hier ist available = false
      System.out.println(Thread.currentThread().getName() + " ends waiting");
      content++;
      // dadurch wird available true !
      System.out.println(Thread.currentThread().getName() + " puts 1 content = " + content);
      available = true;
      notifyAll();
      return 1;
   }

   public synchronized int get()
   {
      // solange nichts da ist, kann nichts abgeholt werden, also warten
      while(available == false)
      {
         System.out.println(Thread.currentThread().getName() + " must wait");
         try
         {
            wait();
         }
         catch(InterruptedException ex)
         {
            return -1;
         }
      }
      System.out.println(Thread.currentThread().getName() + " ends waiting");
      // available = true
      content--;
      System.out.println(Thread.currentThread().getName() + " gets 1 content = " + content);
      available = false;
      notifyAll();
      return 1;
   }
}

Der Consumer
Falls der Interrupt während der wait-Phase ankommt, liefert get -1 zurück.

import java.util.concurrent.TimeUnit;

public class Consumer extends Thread
{
   private Stock lager;
   private int sleepTime;

   public Consumer(Stock c, String name, int sleepTime)
   {
      lager = c;
      this.setName(name);
      this.sleepTime = sleepTime;
   }

   public void run()
   {
      int value=0;
      for(;;)
      {
         value = lager.get(); // 1 abholen, blockiert
         if (value==-1)  // get hat interrupt bekommen
         {
            System.out.println("consumer: wait interrupted, end consumer");
            return;
         }
         try
         {
            //TimeUnit.MILLISECONDS.sleep(1 + (int)(Math.random()*sleepTime));
            Thread.sleep(sleepTime);
         }
         catch(InterruptedException ex)
         {
            interrupt();
            System.out.println("consumer: sleep interrupted, end consumer");
            break;
         }
      }
   }
}

Der Producer
Falls der Interrupt während der wait-Phase ankommt, liefert get -1 zurück.

import java.util.concurrent.TimeUnit;

public class Producer extends Thread
{
   private Stock lager;
   private int sleepTime;

   public Producer(Stock lager, String name, int sleepTime)
   {
      this.lager = lager;
      this.setName(name);
      this.sleepTime = sleepTime;
   }

   public void run()
   {
      int ret = 0;
      for(;;)
      {
         ret = lager.put(); // auffüllen um 1, blockiert
         if (ret == -1)  // put hat interrupt bekommen
         {
            System.out.println("producer: wait interrupted, end producer");
            return;
         }
         try
         {
            //TimeUnit.MILLISECONDS.sleep(1 + (int)(Math.random()*sleepTime));
            Thread.sleep(sleepTime);

         }
         catch(InterruptedException ex)
         {
            interrupt();
            System.out.println("producer: sleep interrupted, end producer");
            break;
         }
      }
   }
}

Main

public class Main
{
   public static void main(String[] args)
   {
      Stock lager = new Stock(5);

      Consumer c1 = new Consumer(lager, "consumer", 20);  // Consumer will mehr verbrauchen als produziert wird
      Producer p1 = new Producer(lager, "producer", 100);

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

      try
      {
         Thread.sleep(500);
         c1.interrupt();
         p1.interrupt();
      }
      catch(InterruptedException e)
      {
         e.printStackTrace();
      }
   }
}

Ablauf für verschiedene, aber feste sleep()-Zeiten
producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
consumer must wait
producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
consumer must wait
producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
consumer must wait
producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
consumer must wait
consumer: wait interrupted, end consumer
producer: sleep interrupted, end producer

Ablauf für zufällig gewählte sleep()-Zeiten

Hier kann es vorkommen, daß die Interrupts Consumer und Producer in der sleep-Phase unterbrechen

producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
consumer must wait
producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
consumer must wait
producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
consumer must wait
producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
consumer must wait
producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
consumer must wait
producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
consumer must wait
producer ends waiting
producer puts 1 content = 5
consumer ends waiting
consumer gets 1 content = 4
producer: sleep interrupted, end producer
consumer: sleep interrupted, end consumer