Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

 << zurück
Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5
Java ist auch eine Insel

Java ist auch eine Insel
5., akt. und erw. Auflage
1454 S., mit CD, 49,90 Euro
Galileo Computing
ISBN 3-89842-747-1
gp Kapitel 9 Threads und nebenläufige Programmierung
  gp 9.1 Prozesse und Threads
    gp 9.1.1 Wie parallele Programme die Geschwindigkeit steigern können
  gp 9.2 Threads erzeugen
    gp 9.2.1 Threads über die Schnittstelle Runnable implementieren
    gp 9.2.2 Thread mit Runnable starten
    gp 9.2.3 Der Name eines Threads
    gp 9.2.4 Die Klasse Thread erweitern
    gp 9.2.5 Wer bin ich?
  gp 9.3 Der Ausführer (Executor) kommt
    gp 9.3.1 Die Schnittstelle Executor
    gp 9.3.2 Die Thread-Pools
    gp 9.3.3 Threads mit Rückgabe über Callable
    gp 9.3.4 Mehrere Callable abarbeiten
    gp 9.3.5 Mit ScheduledExecutorService wiederholende Ausgaben und Zeitsteuerungen
  gp 9.4 Die Zustände eines Threads
    gp 9.4.1 Threads schlafen
    gp 9.4.2 Das Ende eines Threads
    gp 9.4.3 UncaughtExceptionHandler für unbehandelte Ausnahmen
    gp 9.4.4 Einen Thread höflich mit Interrupt beenden
    gp 9.4.5 Der stop() von außen und die Rettung mit ThreadDeath
    gp 9.4.6 Ein Rendezvous mit join() und Barrier sowie Austausch mit Exchanger
    gp 9.4.7 Mit yield() auf Rechenzeit verzichten
    gp 9.4.8 Arbeit niederlegen und wieder aufnehmen
    gp 9.4.9 Priorität
    gp 9.4.10 Der Thread ist ein Dämon
  gp 9.5 Synchronisation über kritische Abschnitte
    gp 9.5.1 Gemeinsam genutzte Daten
    gp 9.5.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
    gp 9.5.3 Punkte parallel initialisieren
    gp 9.5.4 i++ sieht atomar aus, ist es aber nicht
    gp 9.5.5 Kritische Abschnitte schützen
    gp 9.5.6 Schützen mit ReentrantLock
    gp 9.5.7 Synchronisieren mit synchronized
    gp 9.5.8 Synchronized-Methoden der Klasse StringBuffer
    gp 9.5.9 Mit synchronized synchronisierte Blöcke
    gp 9.5.10 Look-Freigabe im Fall von Exceptions
    gp 9.5.11 Mit synchronized nachträglich synchronisieren
    gp 9.5.12 Monitore sind reentrant, gut für die Geschwindigkeit
    gp 9.5.13 Synchronisierte Methodenaufrufe zusammenfassen
    gp 9.5.14 Deadlocks
    gp 9.5.15 Erkennen von Deadlocks
  gp 9.6 Synchronisation über Warten und Benachrichtigen
    gp 9.6.1 Die Schnittstelle Condition
    gp 9.6.2 Beispiel Erzeuger-Verbraucher-Programm
    gp 9.6.3 Warten mit wait() und Aufwecken mit notify()
    gp 9.6.4 Falls der Lock fehlt: IllegalMonitorStateException
    gp 9.6.5 Semaphoren
  gp 9.7 Atomares und frische Werte mit volatile
    gp 9.7.1 Der Modifizierer volatile bei Objekt-/Klassenvariablen
    gp 9.7.2 Das Paket java.util.concurrent.atomic
  gp 9.8 Mit dem Thread verbundene Variablen
    gp 9.8.1 ThreadLocal
    gp 9.8.2 InheritableThreadLocal
  gp 9.9 Gruppen von Threads in einer Thread-Gruppe
    gp 9.9.1 Aktive Threads in der Umgebung
    gp 9.9.2 Etwas über die aktuelle Thread-Gruppe herausfinden
    gp 9.9.3 Threads in einer Thread-Gruppe anlegen
    gp 9.9.4 Methoden von Thread und ThreadGroup im Vergleich
  gp 9.10 Die Klassen Timer und TimerTask
    gp 9.10.1 Job-Scheduler Quartz
  gp 9.11 Einen Abbruch der virtuellen Maschine erkennen


Galileo Computing

9.6 Synchronisation über Warten und Benachrichtigedowntop

Die Synchronisation von Methoden oder Blöcken ist eine einfache Möglichkeit, konkurrierende Zugriffe von der virtuellen Maschine auflösen zu lassen. Obwohl die Umsetzung mit den Locks die Programmierung einfach macht, reicht dies für viele Aufgabenstellungen nicht aus. Wir können zwar Daten in einer synchronisierten Abfolge austauschen, doch gerne möchte ein Thread das Ankommen von Informationen signalisieren, und andere Threads wollen informiert werden, wenn Daten bereit stehen und abgeholt werden können.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Bei der Realisierung der Benachrichtigungen gibt es eine Reihe von Möglichkeiten. Im Folgenden nennen wir die einfachsten:

gp  Jedes Objekt besitzt über die Klasse java.lang.Object die Methoden wait() und notify(). Ein Thread, der über den Monitor verfügt, kann die Methoden aufrufen und sich so in einen Wartezustand versetzen oder einen anderen Thread aufwecken. Diese Möglichkeit gibt es seit Java 1.0. (Es ist schon ein wenig seltsam, dass Java für die Synchronisation ein eingebautes Schlüsselwort hat, aber die Benachrichtigung über Methoden realisiert.)
gp  Von einem ReentrantLock – der den Monitor repräsentiert – liefert newCondition() ein Condition-Objekt, das über await() und signal() warten und benachrichtigen lässt. Diese Typen gibt es seit Java 5.

Szenarien mit Warten und Benachrichtigen sind oft Produzenten-Konsumenten-Beispiele. Ein Thread liefert Daten, die ein anderer Thread verwenden möchte. Da er in keiner kostspieligen Schleife auf die Information warten soll, synchronisieren sich die Partner über ein beiden bekanntes Objekt. Erst wenn der Produzent sein OK gegeben hat, ergibt es für den Datennutzer Sinn, weiterzuarbeiten; jetzt hat er seine benötigten Daten. So wird keine unnötige Zeit in Warteschleifen vergeudet, und der Prozessor kann die übrige Zeit anderen Threads zuteilen.


Galileo Computing

9.6.1 Die Schnittstelle Condition  downtop

Mit einem Lock-Objekt wie ReentrantLock können zwecks Benachrichtigung Condition-Objekte abgeleitet werden. Dazu dient die Funktion newCondition():


interface java.util.concurrent.locks.Lock

gp  Condition newCondition() Liefert ein Condition-Objekt, das mit dem Lock verbunden ist. Mit einem Lock-Objekt können beliebig viele Condition-Objekte gebildet werden.

Warten mit await() und Aufwecken mit signal()

Damit das Warten und Benachrichtigen funktioniert, kommunizieren die Parteien über ein gemeinsames Condition-Objekt, das vom Lock erfragt wird.

Condition condition = lock.newCondition();

In einem fiktiven Szenario soll ein Thread T1 auf ein Signal warten und ein Thread T2 dieses Signal geben. Da nun beide Threads Zugriff auf das gemeinsame Condition-Objekt haben, kann T1 sich mit folgender Anweisung in den Schlaf begeben:

try {
  condition.  await  ();
} catch ( InterruptedException e ) {
  ...
}

Mit dem await() geht der Thread in den Zustand nicht ausführend über. Der Grund für den try/catch-Block ist, dass ein await() durch eine InterruptedException vorzeitig abgebrochen werden kann. Das passiert zum Beispiel, wenn der wartende Thread per interrupt()-Methode ein Hinweis zum Abbruch bekommt.

Die Methode await() bestimmt den ersten Teil des Paares. Der zweite Thread T2 kann nun nach Eintreffen einer Bedingung das Signal geben:

condition.signal();

Um das signal() muss es keinen eine Exception auffangenden Block geben.


Hinweis   Wenn ein Thread ein signal() auslöst und es keinen wartenden Thread gibt, dann verhallt das signal() ungehört. Der Hinweis wird nicht gespeichert und ein nachfolgendes await() muss mit einem neuen signal() aufgeweckt werden.


interface java.util.concurrent.lock.Condition

gp  void await() throws InterruptedException Wartet auf ein Signal, oder die Methode wird unterbrochen.
gp  void signal() Weckt einen wartenden Thread auf.

Vor der Condition kommt ein Lock

Auf den ersten Blick scheint es, als ob das Lock-Objekt nur die Aufgabe hat, ein Condition-Objekt herzugeben. Das ist aber noch nicht alles, denn die Methoden await() und auch signal() können nur dann aufgerufen werden, wenn vorher ein lock() den Signal-Block exklusiv sperrt.

lock.lock();
try {
  condition.  await  ();
} catch ( InterruptedException e ) {
  ...
}
finally {
  lock.unlock();
}

Doch was passiert ohne Aufruf von lock()? Zwei Zeilen zeigen die Auswirkung:

Listing 9.22   AwaitButNoLock.java, main()

Condition condition = new ReentrantLock().newCondition();
condition.await();     // java.lang.IllegalMonitorStateException

Das Ergebnis ist eine java.lang.IllegalMonitorStateException.

Temporäre Lock-Freigabe bei await()

Um auf den Condition-Objekten also await() und signal() aufrufen zu können, ist ein vorangehender Lock nötig. Doch Moment: Wenn ein await() kommt, hält der Thread doch den Monitor, und kein anderer Thread könnte in einen kritischen Abschnitt, der über das gleiche Lock-Objekt gesperrt ist, und signal() aufrufen. Wie ist das möglich? Die Lösung besteht darin, dass await() den Monitor freigibt und den Threads so lange sperrt, bis zum Beispiel von einem anderen Thread das signal() kommt. (Wenn wir ein Programm mit nur einem Thread haben, dann ergibt natürlich so ein Pärchen keinen Sinn.) Kommt das Signal, weckt das den wartenden Thread wieder auf, und er kann am Scheduling wieder teilnehmen.

Mehrere Wartende und signalAll()

Es kann durchaus vorkommen, dass mehrere Threads in einer Warteposition an demselben Objekt sind und aufgeweckt werden wollen. signal() wählt dann aus der Liste der Wartenden einen Thread aus und gibt ihm das Signal. Sollten alle Wartenden einen Hinweis bekommen, lässt sich signalAll() verwenden.


interface java.util.concurrent.lock.Condition

gp  void signalAll() Weckt alle wartenden Hunde auf.

wait() mit einer Zeitspanne

Ein await() wartet im schlechtesten Fall bis zum Nimmerleinstag, wenn es kein signal() gibt. Es gibt jedoch Situationen, in denen wir eine bestimmte Zeit lang warten, aber bei Fehlen der Benachrichtigung weitermachen wollen. Dazu kann dem await() in unterschiedlichen Formen eine Zeit mitgegeben werden.


interface java.util.concurrent.lock.Condition

gp  long awaitNanos( long nanosTimeout ) throws InterruptedException Wartet eine bestimmte Anzahl Nanosekunden auf ein Signal, oder die Methode wird unterbrochen. Die Rückgabe gibt die Wartezeit an.
gp  boolean await( long time, TimeUnit unit ) throws InterruptedException
gp  boolean awaitUntil( Date deadline ) throws InterruptedException Wartet eine bestimmte Zeit auf ein Signal. Kommt das Signal in der Zeit nicht, geht die Methode weiter und liefert true. Kam das Signal oder ein interrupt(), liefert die Methode false.
gp  void awaitUninterruptibly() Wartet ausschließlich auf ein Signal und lässt sich nicht durch ein interrupt() beenden.

An den Methoden ist schon zu erkennen, dass die Wartezeit einmal relativ (await()) und einmal absolut (awaitUntil()) sein kann. (Mit den eingebauten Methoden wait() und notify() ist immer nur eine relative Angabe möglich.)

Eine IllegalMonitorStateException wird das Ergebnis sein, wenn beim Aufruf einer Condition-Methode das lock() des zugrunde liegenden Lock-Objekts fehlte.


Beispiel   Warte maximal zwei Sekunden auf das Singnal über condition1. Wenn es nicht ankommt, versuche signal()/signalAll() von condition2 zu bekommen.
condition1.await( 2, TimeUnit.SECONDS );
condition2.await();
Die Ausnahmebehandlung muss bei einem lauffähigen Beispiel noch hinzugefügt werden.


Galileo Computing

9.6.2 Beispiel Erzeuger-Verbraucher-Programm  downtop

Ein kleines Erzeuger-Verbraucher-Programm soll die Anwendung von Threads kurz demonstrieren. Zwei Threads greifen auf eine gemeinsame Datenbasis zurück. Ein Thread produziert unentwegt Daten (in dem Beispiel ein Zeit-Datum) und schreibt sie in eine Queue. Der andere Thread nimmt Daten aus der Queue heraus und schreibt sie auf den Bildschirm.

Beginnen wir mit einer Klasse Lager, die mit den Methoden rein() und raus() Daten abspeichern und ausgeben kann. Die Größe des Lagers ist beschränkt und so blockiert die Methode rein(), wenn mehr als ein definiertes Maximum von Elementen gespeichert ist. (O.k. Wir hätten die Elemente auch verwerfen können, aber das ist hier keine gute Idee.) Eine zweite Wartesituation haben wir auch noch: Ist das Lager leer, kann raus() kein Element geben.

Listing 9.23   ErzeugerVerbraucherDemo.java, Teil 1

import java.util.*;
import java.util.concurrent.locks.*;
class Lager<T>
{
  private static final int MAXQUEUE = 10;
  private final Queue<T> produkte    = new LinkedList<T>();
  private final Lock lock            = new ReentrantLock();
  private final Condition nichtVoll  = lock.newCondition();
  private final Condition nichtLeer  = lock.newCondition();
  public void rein( T elem ) throws InterruptedException
  {
    lock.lock();
    try
    {
      while ( produkte.size() == MAXQUEUE )
        nichtVoll.await();
      produkte.add( elem );
      nichtLeer.signalAll();
    }
    finally {
      lock.unlock();
    }
  }
  public T raus() throws InterruptedException
  {
    lock.lock();
    try
    {
      while ( produkte.size() == 0 )
        nichtLeer.await();
      T elem = produkte.poll();
      nichtVoll.signalAll();
      return elem;
    }
    finally {
      lock.unlock();
    }
  }
}

Als Objektvariable wird eine Warteschlange als Objekt vom Typ Queue definiert, das die Daten aufnimmt, auf die die Threads dann zurückgreifen. Die erste definierte Funktion ist rein(). Wenn noch Platz in der Warteschlange ist, dann hängt die Funktion das Objekt an. Ist das Lager voll, muss gewartet werden, bis es jemand leert.

while ( produkte.size() == MAXQUEUE )
  nichtVoll.await();

Es ist typisch für Wartesituationen, dass await() in einem Schleifenrumpf aufgerufen wird. Denn falls ein signalAll() aus dem await() erlöst, muss weiterhin getestet werden, ob die Bedingung immer noch gilt. Ein einfaches if würde dazu führen, dass bei zwei aufgeweckten Threads beide glauben, dass die Queue jeweils ein Element, also in der Summe zwei, aufnehmen kann. Doch bei nur einem entnommenen Element – und dem damit verbundenen signalAll() –, darf auch nur ein Element wieder hinein. Die Schleife verhindert, dass jeder Geweckte ein Element hineinlegt.

Bei der Funktion raus() finden wir das gleiche Muster. Sind in der Warteschlange keine Daten vorhanden, so muss der interessierte Thread warten. Kommt ein Element hinein, kann genau eine herausgenommen werden. Eine einfache Fallunterscheidung könnte bei zwei Wartenden und einem signalAll() vom neuen Element dazu führen, dass beide Threads ein Element entnehmen. Das geht aber nicht, da nur ein Element in der Queue ist.

Die Schleifenbedingung – etwa produkte.size() == 0 – ist das Gegenteil der Bedingung, auf die gewartet werden soll; Produktanzahl ungleich null, dann geht’s weiter.

Die allgemeine Lager-Klasse ist ein Beispiel für eine so genannte beschränkte blockierende Queue. Java 5 definiert mit BlockingQueue eine Schnittstelle für blockierende Operationen. Die Klasse ArrayBlockingQueue ist eine solche Warteschlage, die blockiert, wenn keine Daten enthalten sind und ein Maximum erreicht ist. Jetzt haben wir uns eine einfache Variante einer solchen Datenstruktur selbst gebaut!

Im der Quellcodedatei folgen Erzeuger und Verbraucher. Als Erstes wird das Lager-Objekt gebaut, über welches Erzeuger und Verbraucher ihre Daten austauschen.

public class ErzeugerVerbraucherDemo
{
  public static void main( String[] args )
  {
    final Lager<String> lager = new Lager<String>();
    new Thread() // Erzeuger
    {
      @Override public void run()
      {
        try
        {
          while ( true )
          {
            String s = String.format( "%1$tT, %1$tL"new Date() );
            lager.rein( s );
            System.out.println( "Produziere: " + s );
            sleep( (int) (Math.random() * 100) );
          }
        }
        catch ( InterruptedException e ) { e.printStackTrace(); }
      }
    }.start();

Der Erzeuger ist ein Thread, der unermüdlich Zeit-Strings produziert und in die Queue schreibt. Dann lässt er sich etwas Zeit bis zum nächsten String.

Der letzte Teil ist der Verbraucher; ein Thread, der dem Lager ständig Dinge entnimmt.

    class Verbraucher extends Thread
    {
      final private String name;
      Verbraucher( String name )
      {
        this.name = name;
      }
      @Override public void run()
      {
        try
        {
          while ( true )
          {
            System.out.println( name + " holt Nachricht: " + lager.raus() );
            Thread.sleep( (int) (Math.random() * 1000) );
          }
        }
        catch ( InterruptedException e ) { e.printStackTrace(); }
      }
    }
    new Verbraucher( "Eins" ).start();
    new Verbraucher( "Zwei" ).start();
    new Verbraucher( "Drei" ).start();
  }
}

Das Programm startet drei Verbraucher, was eine Ausgabe ähnlich dieser ergibt:

Eins holt Nachricht: 21:26:03285
Produziere: 21:26:03285
Zwei holt Nachricht: 21:26:03365
Produziere: 21:26:03365
Drei holt Nachricht: 21:26:03415
Produziere: 21:26:03415
Produziere: 21:26:03425
Zwei holt Nachricht: 21:26:03425
Produziere: 21:26:03525
Produziere: 21:26:03596
Produziere: 21:26:03696
Produziere: 21:26:03766
Produziere: 21:26:03846
Drei holt Nachricht: 21:26:03525
Produziere: 21:26:03936
Produziere: 21:26:03976
Zwei holt Nachricht: 21:26:03596
Produziere: 21:26:04016
Produziere: 21:26:04096
Produziere: 21:26:04126
Produziere: 21:26:04156
Eins holt Nachricht: 21:26:03696
Produziere: 21:26:04216
Produziere: 21:26:04246
Eins holt Nachricht: 21:26:03766
Produziere: 21:26:04317
Produziere: 21:26:04337
Eins holt Nachricht: 21:26:03846
Drei holt Nachricht: 21:26:03936
Produziere: 21:26:04667
Zwei holt Nachricht: 21:26:03976
Eins holt Nachricht: 21:26:04016
Produziere: 21:26:04927
... bis in die Unendlichkeit

Da die Konsolenausgabe nicht-synchronisiert ist, hat es den Anschein, als ob erst konsumiert und dann produziert wird!


Galileo Computing

9.6.3 Warten mit wait() und Aufwecken mit notify()  downtop

Nachdem im vorigen Kapitel der Weg mit den Java-5-Klassen und -Schnittstellen beschritten wurde, wollen wir uns abschließend mit den Möglichkeiten beschäftigen, die Java seit der Version 1.0 mitbringt.

Nehmen wir wieder zwei Threads an. Sie sollen sich am Objekt o synchronisieren – die Methoden wait() und notify() sind nur mit dem entsprechenden Monitor gültig, und den besitzt das Programmstück, wenn es sich in einem synchronisierten Block aufhält. Thread T1 soll auf Daten warten, die Thread T2 liefert. T1 führt dann etwa den folgenden Programmcode aus:

  synchronized( o )
  {
  try {
      o.wait()  ;
    // Habe gewartet, kann jetzt loslegen.
  } catch ( InterruptedException e ) {
    ...
  }
}

Wenn der zweite Thread den Monitor des Objekts o bekommt, kann er den wartenden Thread aufwecken. Er bekommt den Monitor durch das Synchronisieren der Methode, was ja bei Objektmethoden synchronized(this) entspricht. T2 gibt das Signal mit notify():

  synchronized( o )
  {
  // Habe etwas gemacht und informiere jetzt meinen Wartenden.
    o.notify()  ;
}

Um notify() muss es keinen eine Exception auffangenden Block geben. Wenn ein Thread ein notify() auslöst und es keinen wartenden Thread gibt, dann verpufft es.


class java.lang.Object

gp  void wait() throws InterruptedException Der aktuelle Thread wartet an dem aufrufenden Objekt darauf, dass er nach einem notify()/notifyAll() weiterarbeiten kann. Der aktive Thread muss natürlich den Monitor des Objekts belegt haben. Andernfalls kommt es zu einer IllegalMonitorStateException.
gp  void wait( long timeout ) throws InterruptedException Wartet auf ein notify()/notifyAll() eine gegebene Anzahl von Millisekunden. Nach Ablauf dieser Zeit geht es ohne Fehler weiter.
gp  void wait( long timeout, int nanos ) throws InterruptedException Wartet auf ein notify()/notifyAll() – angenähert 1 000 000 – timeout + nanos Nano-Sekunden.
gp  void notify() Weckt einen beliebigen Thread auf, der an diesem Objekt wartet.
gp  void notifyAll() Benachrichtigt alle Threads, die auf dieses Objekt warten.

Ein wait() kann mit einer InterruptedException vorzeitig abbrechen, wenn der wartende Thread per interrupt()-Methode unterbrochen wird. Die Tatsache, dass wait() temporär den Lock freigibt, was für uns mit synchronized aber nicht möglich ist, spricht dafür, dass etwas wie wait() nativ implementiert werden muss.


Galileo Computing

9.6.4 Falls der Lock fehlt: IllegalMonitorStateException  downtop

Wenn wait() oder notify() aufgerufen werden, uns aber der entsprechende Lock für das Objekt fehlt, kommt es zum Laufzeitfehler IllegalMonitorStateException, wie wir es schon bei Condition und dem fehlenden lock() vom Lock gesehen haben.

Was wird bei folgendem Programm passieren?

Listing 9.24   NotOwner.java

class NotOwner
{
  public static void main( String[] args ) throws InterruptedException
  {
    new NotOwner().wait();
  }
}

Der Compiler kann das Programm übersetzen, doch zur Laufzeit wird es zu einem Fehler kommen:

java.lang.IllegalMonitorStateException: current thread not owner
  at java.lang.Object.wait(Native Method)
  at java.lang.Object.wait(Object.java:426)
  at NotOwner.main(NotOwner.java:5)
Exception in thread "main"

Der Fehler zeigt an, dass der aktuelle ausführende Thread (current thread) nicht den nötigen Lock besitzt, um wait() auszuführen. Das Problem ist hier mit einem synchronized-Block (oder Methode) zu lösen. Um den Fehler zu beheben, setzen wir:

NotOwner o = new NotOwner();
synchronized( o )
{
  o.wait();
}

Das zeigt, dass das Objekt o, das den Lock besitzt, für ein wait() »bereit« sein muss. In die richtige Stimmung wird es nur mit synchronized gebracht.

synchronized( NotOwner.class )
{
  new NotOwner().wait();
}

Doch natürlich könnten wir auch am Klassenobjekt synchronisieren:

synchronized( NotOwner.class )
{
  NotOwner.class.wait();
}

Beispiel   Die Ähnlichkeit zwischen Lock auf der einen Seite und einem synchronisierten Block bzw. Methode auf der anderen und den Methoden wait() und notify() bei Object und den analogen Methoden await() und signal() bei den Condition-Objekten ist nicht zu übersehen. Auch der Fehler beim Fehlen des Monitors ist der gleiche: ein Aufruf der Methoden await()/wait() und notify()/signal() führt zu einer IllegalMonitorStateException. Es muss also erst ein synchronisierter Block für den Monitor her oder ein Aufruf lock() auf dem Condition zugrunde liegenden Lock-Objekt.


Galileo Computing

9.6.5 Semaphoren  toptop

Synchronisationsprobleme können mittels kritischer Abschnitte und Wartesituationen mit wait() und notify() gelöst werden. Dennoch ist der eingebaute Mechanismus auch mit Nachteilen verbunden. Die große Schwierigkeit ist es, synchronisierende Programme zu entwickeln und zu warten, denn die Synchronisationsvariablen verstreuen sich mitunter über große Programmteile und machen die Wartung schwierig. Teile, die bewusst atomar ausgeführt werden müssen, benötigen zwingend einen Programmblock und eine Synchronisationsvariable. Und das heißt für uns Entwickler, dass wir einen vorher einfachen Block durch wait() und notify() ersetzen müssen, der synchronisiert ist. Und wir müssen uns um eine Variable kümmern. Das ist unangenehm, und wir wünschen uns ein einfacheres Konzept, sodass eine Umstellung leicht ist. Hier bieten sich Funktionsaufrufe an. Es ist schön, die Wartesituation hinter einem Paar von Funktionen wie enter() und leave() zu verstecken.

Die Idee für diese Realisierung stammt vom niederländischen Informatiker Edsger Wybe Dijkstra. Neben vielen anderen Problemen der Informatik beschäftigte er sich mit der Wahl der kürzesten Wege und mit der Synchronisation von Prozessen. Zur damaligen Zeit wurde Parallelität noch mit Hilfe von Variablen und Warteschleifen realisiert; Programmiersprachen mit höheren Konzepten, wie sie Java bietet, waren nicht verbreitet. Dijkstra schlug einen Satz von Funktionen P() und V() vor, die das Eintreten und Verlassen in und aus einem atomaren Block umsetzen. Dijkstra assoziierte mit den Funktionsnamen die Wörter pass und vrij, was auf Niederländisch frei heißt. Er nahm zur Verdeutlichung ein Beispiel aus dem Eisenbahnverkehr. Dort darf sich nur ein Zug auf einem Streckenabschnitt aufhalten, wenn ein zweiter Zug einfahren will, muss er warten und kann nur weiterfahren, wenn der erste Zug die Strecke verlassen hat.

Eine Semaphore-Klasse seit Java 5

Um zu kontrollieren, wie viele Threads auf ein Programmstück zugreifen können, kann in Java die Klasse java.util.concurrent.Semaphore verwendet werden. Mit ihr lassen sich zwei Typen von Semaphoren umsetzen:

gp  Binäre Semaphoren lassen höchstens einen Thread auf ein Programmstück zu.
gp  Allgemeine Semaphoren lassen eine bestimmte begrenzte Menge an Threads in einen kritischen Abschnitt. Die Semaphore verwaltet intern eine Menge so genannter Erlaubnisse (eng. permits).

Eine binäre Semaphore wird mit dem klassischem wait() und notify() realisiert. Allgemeine Semaphoren vereinfachen das Konsumenten-Produzenten-Problem, da eine bestimmte Anzahl von Threads in einem Block erlaubt sind. Die verbleibende Größe des Puffers ist somit automatisch die maximale Anzahl von Produzenten, die sich parallel im Einfügeblock befinden können.

Die wichtigen Eigenschaften der Semaphore-Klasse sind der Konstruktor und die Methoden zum Betreten und Verlassen des kritischen Abschnitts.


class java.util.concurrent.Semaphore implements Serializable

gp  Semaphore( int permits ) Eine neue Semaphore, die bestimmt, wie viele Threads in einem Block sein dürfen.
gp  void acquire() Versucht, in den kritischen Block einzutreten. Wenn der gerade belegt ist, wird gewartet. Vermindert die Menge der Erlaubnisse um eins.
gp  void release() Verlässt den kritischen Abschnitt und legt eine Erlaubnis zurück.

Unser Beispiel soll mit einer Semaphore arbeiten, die nur zwei Threads gleichzeitig in den kritischen Abschnitt lässt.

Listing 9.25   SemaphoreDemo.java

import java.util.concurrent.Semaphore;
public class SemaphoreDemo
{
  static   Semaphore semaphore = new Semaphore( 2 );  

Der kritische Abschnitt besteht aus zwei Operationen: eine Ausgabe auf dem Bildschirm und eine Wartezeit von zwei Sekunden. Er ist in einem Runnable eingebettet:

  static Runnable r = new Runnable() {
    public void run() {
      while ( true ) {
        try
        {
            semaphore.acquire();
            System.out.println( "Thread=" + Thread.currentThread().getName() +
                              ", Available Permits=" + semaphore.availablePermits() );
          Thread.sleep( 2000 );
        }
        catch ( InterruptedException e ) {
          e.printStackTrace();
        }
        finally {
            semaphore.release();
          }
      }
    }
  };

Der kritische Abschnitt beginnt mit dem acquire() und endet mit release() im finally. In der Ereignisbehandlung fangen wir eine mögliche InterruptedException von acquire() und Thread.sleep() auf. Das release() ist im finally sehr gut aufgehoben, denn wir wollen in jedem Fall, auch wenn irgendwie eine andere RuntimeException auftauchen sollte, den Lock wieder freigeben.

Im letzten Teil starten wir einfach drei Threads:

  public static void main( String[] args )
  {
    new Thread( r ).start();
    new Thread( r ).start();
    new Thread( r ).start();
  }
}

Nach dem Starten ist gut zu beobachten, wie jeweils zwei Threads im Abschnitt sind (eine Leerzeile symbolisiert die Wartezeit):

Thread=Thread-0Available Permits=1
Thread=Thread-1Available Permits=0
Thread=Thread-2Available Permits=0
Thread=Thread-0Available Permits=0
Thread=Thread-2Available Permits=0
Thread=Thread-0Available Permits=0

Fair und unfair

In der Ausgabe ist zu sehen, dass Thread 0, 1 und 2 zwar ihre Aufgaben ausführen können, aber plötzlich eine Sequenz 0, 2, 0 entsteht. Unser Gerechtigkeitssinn sagt uns jedoch, dass Thread 1 wieder an die Reihe kommen müsste. Wie ist das möglich? Die Antwort lautet, dass das acquire() nicht berücksichtigt, wer am längsten wartet, sondern dass es sich aus der Liste der Wartenden einen beliebigen Thread auswählt. (Wir kennen das von notify() her und dem Betreten eines synchronized Blocks.) Um ein faires Verhalten zu realisieren, wird die Fairness einfach über den Konstruktor von Semaphore angegeben. Ändern wir im Programm folgende Zeile:

static Semaphore semaphore = new Semaphore( 2,   true   );

Nun bekommen wir folgenden Ausgabe:

Thread=Thread-0Available Permits=1
Thread=Thread-1Available Permits=0
Thread=Thread-2Available Permits=0
Thread=Thread-0Available Permits=0
Thread=Thread-1Available Permits=0
Thread=Thread-2Available Permits=0
Thread=Thread-0Available Permits=0
Thread=Thread-1Available Permits=0



1  Holland ist im Übrigen nur eine Provinz der Niederlande

2  Einige Infos über ihn unter http//:henson.cc.kzoo.edu/-k98mn01/dijkstra.html

 << zurück




Copyright © Galileo Press GmbH 2005
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de