Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | NEWSLETTER | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
NEWSLETTER 
CONTACT 
Java Multithread Support - Nested Monitor Problem

Java Multithread Support - Nested Monitor Problem
Java Multithread Support
Das Nested-Monitor-Problem

JavaSPEKTRUM, September 2004
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 

In unserem letzten Artikel / KRE3 / haben wir uns angesehen, wie ein Thread durch Aufruf der Methode Object.wait() auf das Eintreten einer logischen Bedingung warten kann, die ihm ein anderer Thread durch Aufruf der Methode Object.notify() oder Object.notifyAll() signalisiert. Dabei haben wir für das Warten und Signalisieren mehrerer logischer Bedingungen immer nur ein einziges Bedingungsobjekt verwendet. In diesem Artikel wollen wir uns ansehen, warum man das so macht bzw. warum die Verwendung mehrerer Bedingungsobjekte bei der Benutzung von wait() und notify() (bzw notifyAll()) in der Regel zu Problemen (dem sogenannten Nested-Monitor-Problem) führt. Am Ende des Artikel schauen wir uns dann noch an, wie die Neuerungen im JDK 1.5 dazu führen, dass das bisherige Standardvorgehen bei der Benutzung von Bedingungen in Java sich geändert hat.
 

Rückblick: der BlockingIntStack

In Multithread-Umgebungen können sich Threads über das Eintreten von logischen Bedingungen verständigen. Dabei warten ein oder mehrere Threads durch Aufruf der Methode Object.wait() auf das Eintreten einer logischen Bedingung. Das Eintreten der Bedingung wird von einem oder mehreren anderen Threads erkannt und den wartenden Threads mit Hilfe von Object.notify() oder Object.notifyAll() signalisiert. Als Beispiel für die Nutzung dieser Technik haben wir einen BlockingIntStack implementiert. Dieser Stack erlaubt es einem Thread, der mit pop() einen int-Wert aus einem leeren Stack holen will, zu warten, bis ein anderer Thread, der mit push() einen int-Wert geschrieben hat, ihm signalisiert, dass nun ein Element im Stack vorhanden ist. Zur Ergänzung, hier noch einmal der Sourcecode:
public class BlockingIntStack {
 private final int[] array;
 private volatile int cnt = 0;

 public IntStack (int sz) { array = new int[sz]; }

synchronized public void push (int elm) throws InterruptedException {
   while (cnt == array.length)
     wait();
   if (cnt == 0)
     notifyAll();
   array[cnt++] = elm;
 }

 synchronized public int pop () throws InterruptedException {
   while (cnt == 0)
     wait();
   if (cnt == array.length)
     notifyAll();
   return(array[--cnt]);
 }

 public int size() { return cnt; }

 public int capacity() { return (array.length); }

}

Wie man an der Implementierung sehen kann, hat eine Instanz eines BlockingIntStack eine feste Größe und wächst nicht dynamisch. Deshalb erlaubt der BlockingIntStack einem Thread, der in einem vollen Stack noch ein int-Wert mit push() ablegen will, zu warten, bis ein anderer Thread einen int-Wert mit pop() herausgeholt hat und ihm signalisiert, dass nun wieder Platz im Stack ist.

Das Objekt auf dem wait() und notifyAll() aufgerufen wird, ist in der obigen Implementierung immer dasselbe, nämlich this. Dies gilt sowohl für das Warten und Notifizieren bei einem vollen wie auch bei einem leeren Stack. Das Objekt, das für das Warten/Notifizieren genutzt wird, nennt man in Anlehnung an Thread APIs, die vor Java existiert haben, Bedingung (englisch: condition). Das heißt, in unserer Implementierung werden die zwei logischen Bedingungen: ‚nicht mehr voll’ und ‚nicht mehr leer’ an einem einzigen Bedingungsobjekt (nämlich this) kommuniziert.

Ein einziges Bedingungsobjekt für verschiedene logische Bedingungen zu verwenden ist recht typisch für die Benutzung von wait() und notify() (bzw notifyAll()) in Java. Warum das so ist, bzw. warum das in den meisten Fällen sogar so sein muss, wollen wir in diesem Artikel untersuchen.
 

Das Nested-Monitor-Problem

Fangen wir damit an, dass wir versuchen den BlockingIntStack so zu ändern, dass er je ein Bedingungsobjekt für "nicht mehr voll" und "nicht mehr leer" hat. In der ursprünglichen Implementierung wurde this als Bedingungsobjekt verwendet. Da wir nun zwei Bedingungsobjekte verwenden wollen, können wir this allein nicht mehr verwenden. Statt dessen werden wir zwei zusätzliche private Attribute als Bedingungsobjekte nutzten, so dass unsere Klasse folgendermaßen aussieht (push() und pop() fehlen noch):
public class BlockingIntStackWithTwoCondtions {
 private final Object fullCondition = new Object();
 private final Object emptyCondition = new Object();

 private final int[] array;
 private volatile int cnt = 0;

 public IntStack (int sz) { array = new int[sz]; }

 public int size() { return cnt; }

 public int capacity() { return (array.length); }
}

Bevor wir zur Implementierung von push() und pop() kommen, wollen wir uns noch einmal in Erinnerung rufen, dass das Mutex des Bedingungsobjekt gesperrt sein muss, bevor wait() oder notifyAll() (bzw. notify()) darauf aufgerufen werden kann. Das bedeutet, dass die jeweiligen Aufrufe von wait() und notifyAll() in einem entsprechenden synchronized-Block stehen müssen, der fullCondition bzw. emptyCondition als Mutex nutzt. Gleichzeitig benötigen wir die Sperre eines Mutex, um den Stackpointer cnt und den Speicher array atomar zu manipulieren (siehe / KRE1 /).

Eine mögliche Implementierung von push() und pop(), die diesen Anforderungen gerecht wird, sieht folgendermaßen aus:

public void push (int elm) throws InterruptedException {
 synchronized(fullCondition) {
  synchronized(emptyCondition) {
   while (cnt == array.length)
     fullConnection.wait();
   if (cnt == 0)
     emptyConnection.notifyAll();
   array[cnt++] = elm;
  }
 }
}

public int pop () throws InterruptedException {
 synchronized(fullCondition) {
  synchronized(emptyCondition) {
   while (cnt == 0)
     emptyConnection.wait();
   if (cnt == array.length)
     fullConnection.notifyAll();
   return(array[--cnt]);
  }
 }
}

Hier wird jeweils die gesamte Methode mit beiden Mutexen gesperrt. Wichtig ist dabei, dass das Sperren der Mutexe in beiden Methoden in gleicher Reihenfolge geschieht, sonst besteht potenziell die Gefahr eines Deadlocks. Deshalb ist jeweils der synchronized-Block mit emptyCondition in den synchronized-Block mit fullCondition geschachtelt. Obwohl der obige Code ganz gut aussieht und alle bisher beschriebenen Anforderungen erfüllt, funktioniert er aus äußerst subtilen Gründen trotzdem nicht.

Bevor wir diese Gründe im Detail diskutieren, erinnern wir uns noch einmal daran, was wir über das Zusammenspiel von wait() und notify() (bzw. notifyAll()) in unserem letzten Artikel gesagt haben: der Thread gibt implizit das Mutex des Bedingungsobjekts frei, wenn er wait() darauf aufruft. Dies ist auch unbedingt nötig, damit der Thread, der notify() (bzw. notifyAll()) aufrufen möchte, das Mutex sperren kann, um danach notify() (bzw. notifyAll()) aufrufen zu können. Detaillierter kann man dies im Sequenzdiagram in Abbildung 1 sehen. Es zeigt einen Produzenten-Thread, der an einem vollen BlockingIntStack (mit nur einer Bedingung) darauf wartet, dass ein Konsumenten-Thread einen Wert herausnimmt.


Abbildung 1: Synchronisation über wait() und notifyAll() unter Verwendung von nur einem Bedingungsobjekt

Funktioniert diese Synchronisation in der gleichen Situation mit unserem BlockingIntStackWithTwoConditions? Nein, leider nicht. Der Produzenten-Thread sperrt erst das fullCondition Mutex und dann das emptyCondition Mutex. Da der Stack bereits voll ist, kommt er in die while-Schleife und ruft fullCondition.wait() auf. Damit gibt der das fullCondition Mutex frei. Man beachte: er hält weiterhin das emptyCondition Mutex. Ruft nun der Konsumenten-Thread pop(), so versucht er nacheinander das fullCondition Mutex und dann das emptyCondition Mutex zu sperren. Beim emptyCondition Mutex bleibt er hängen, weil dies noch dem Produzenten-Thread gehört. Dies ist dann auch der vollständige Stillstand dieses aus Produzenten und Konsumenten bestehenden Systems. Abbildung 2 zeigt diesen Ablauf in einem Sequenzdiagram.


Abbildung 2: Nested-Monitor-Problem: Deadlock bei Verwendung von zwei Bedingungsobjekten

Das gerade beschriebene Problem ist eine wohlbekannte Tatsache in der Java-Multithread-Programmierung - eine Art Anti-Pattern mit dem Namen Geschachtelter-Monitor-Problem (englisch: nested monitor problem).

Skeptiker zweifeln an dieser Stelle vielleicht daran, dass die obige Implementierung von push() und pop() bzgl. des Problems repräsentativ ist. Schließlich gibt es ein ganze Reihe von Variationsmöglichkeiten und darunter könnte ja eine sein, die dieses Problem nicht hat und außerdem noch korrekt ist. Trotzdem gibt es keine Lösung. Wie man es auch dreht und wendet, das Nested-Monitor-Problem läuft im Prinzip immer darauf hinaus, dass beim Aufruf von wait() nur das Mutex desjenigen Bedingungsobjekts frei gegeben wird, auf  dem wait() aufgerufen wird. Wenn aber mehrere Bedingungsobjekte verwendet werden, dann werden  immer auch mehrere Mutexe gehalten; das liegt an der in Java (pre-JDK 1.5) eingebauten 1:1-Beziehung zwischen Bedingung und Mutex, bei der jedes Bedingungsobjekt immer genau das eigene Mutex verwendet. Das Problem läßt sich vermeiden, wenn man mit nur einem Bedingungsobjekt (und seinem Mutex) arbeitet und daran zwei logische Bedingungen kommuniziert werden. Womit wir wieder beim BlockingIntStack vom Anfang wären.

Abweichend von unserer Implementierung  des BlockingIntStack mit einem Bedingungsobjekt ist es natürlich möglich, als Bedingungsobjekt nicht this sondern ein explizites privates Attribut zu verwenden. Die Vor- bzw. Nachteile bei der Benutzung eines expliziten privaten Bedingungsobjekts sind im wesentlichen die gleichen wie die bei der Benutzung eines expliziten privaten Mutexobjekt. Wir haben diese Situation bereits in einem der vorhergehenden Artikel (siehe / KRE2 /) diskutiert.
 

Das Nested-Monitor-Problem in der Java-Programmierpraxis

Die Ursachen der Blockade, in die wir mit unsrem BlockingIntStackWithTwoCondtions gekommen sind, waren relativ einfach zu analysieren, weil beide Bedingungen in derselben Klasse genutzt werden und damit im Code relativ nah beieinander liegen. Ein Blick auf die push() oder die pop()-Methode genügt, um die geschachtelten synchronized Blöcke (die die geschachtelten Monitore repräsentieren) zu erkennen.

Das ist in der Praxis nicht immer so einfach. Betrachten wir eine andere Situation. Nehmen wir zum Beispiel eine Klasse A, deren Methoden mit dem Mutex von this synchronisiert sind. Diese Klasse enthält ein Attribut der Klasse B, die als Bedingungsobjekt wieder this (aber diesmal das des Attributs vom Typ B) nutzt.
 
public class A {
  private B b;
  public synchronized void m1() {
     ...
    b.m3();
     ...
  }
  public synchronized void m2() {
     ...
    b.m4();
     ...
  }
}
public class B {
  public synchronized void m3() {
     ...
     while (...)
       wait();
     ...
  }
  public synchronized void m4() {
     ...
    if (...)
      notifyAll();
     ...
  }
}

Hier haben wir ebenfalls einen geschachtelten Monitor, wenn die synchronisierten Methoden der äußeren Klasse A die Methoden aus B aufrufen, die ihrerseits wait() und notify() (oder notifyAll()) aufrufen.  Das Sequenzdiagramm in Abbildung 3 zeigt den Ablauf.  Es wird zwar nur eine logische Bedingung verwendet (in der Klasse B), aber es werden zwei Monitore (nämlich der des äußeren A -Objekts und der des enthaltenen B -Objekts) benutzt.  Der Aufruf von wait() in der Klasse B gibt nur das Mutex von B frei, aber der andere Thread wartet noch immer auf das Mutex von A und wird niemals dazu kommen, eine Methode von B aufzurufen, die die erwartete Zustandsänderung herbeiführen könnte.  Wir haben also wieder ein Nested-Monitor-Problem.

Zusammenfassung

In dieser Ausgabe haben wir uns die Synchronisation von mehreren Threads über wait() und notify() (bzw. notifyAll()) angesehen.  wait() und notify() werden verwendet, um zustandsabhängige Aktionen zu implementieren.  Wenn eine Aktion abhängig vom Zustands eines Objekts nicht ausgeführt werden kann, dann kann in einer Multithread-Umgebung auf eine Zustandsänderung gewartet werden, statt die Aktion mit einer Fehlerindikation sofort abzubrechen. Die Idee der Kommunikation über wait() und notify() besteht darin, dass ein (oder mehrere) Threads auf die Zustandsänderung warten und ein (oder mehrere) andere Threads ein Signal senden, wenn sie die Änderung herbei geführt haben.

Dabei sind diverse Details zu beachten. Wir haben den Unterschied zwischen notify() und  notifyAll() diskutiert. Dabei hat sich herausgestellt, dass die robusteste Art der Verwendung von wait() und notify() darin besteht, in einer while-Schleife den Zustand abzufragen und danach per wait() zu warten, während der signalisierende Thread die Benachrichtigung per notifyAll() an alle wartenden Threads (und nicht per notify() an nur genau einen Thread) versendet.


Abbildung 3: Nested-Monitor-Problem bei Verwendung von zwei Mutexen

Eine getrennte statische Analyse der beiden Klassen A und B enthüllt das Problem allerdings nicht, da das Thread-Verhalten von dem dynamischen Ablauf (welche Mutexe wurden wann gesperrt?) und weniger von der statischen Struktur innerhalb der einzelnen Klasse abhängt. Dies ist ein Beispiel dafür, dass die Ideen der Objektorientierung (Datenkapselung in Abstraktionen, Polymorphismus in Klassenhierarchien, etc.) und die Ideen der Multithread-Welt eher orthogonal zueinander stehen, als sich zu ergänzen. Verwirrend ist dabei, dass in Java im Vergleich zu anderen Programmiersprachen die statische Struktur innerhalb einer Klasse noch eine relativ große Rolle spielt, da Sperren ausschließlich an Block- bzw. Methodengrenzen aktiviert und deaktiviert werden können (zumindest bis zum JDK 1.5).

Wenn also die statische Analyse eines Nested-Monitor-Problems relativ mühselig ist und eine gewisse Erfahrung mit der Multithread-Programmierung voraussetzt, stellt sich automatisch die Frage, ob es andere Möglichkeiten der Fehleranalyse gibt. Verschiedene Tools (siehe Randbemerkun g "Hilfsmittel für die Deadlock-Analyse") bieten die Möglichkeit, sich die von einem Thread gesperrten Mutexe sowie die Gründe für das Warten eines Threads (Warten auf Mutex-Sperren, Warten an einer Bedinung, usw.) anzusehen. Das heißt, wenn es einen "Hänger" auf Grund eines geschachtelten Monitors gibt, dann kann man sich die wartenden Threads und die von ihnen eventuell gehaltenen Sperren sowie die Gründe für ihr Warten ansehen. Bei richtiger Zuordnung dieser Informationen und dem Wissen darüber, welcher der wartenden Threads potenziell notify() bzw. notifyAll() auf der fraglichen Bedingung aufgerufen haben könnte, lassen sich dann meist die Ursachen für den “Hänger“ finden.

Obwohl Tools, mit denen man sich die Sperren eines Threads ansehen kann, meist auch die Fähigkeit haben, Deadlocks selbständig zu finden, ist die manuelle Analyse im Fall eines Nested-Monitor-Problems unumgänglich. Denn ein geschachtelter Monitor ist im eigentlichen Sinne kein echter Deadlock. Bei einem echten Deadlock wartet ein Thread auf die Freigabe einer Sperre (englisch: lock), die von einem anderen Threads gehalten wird, aber nicht mehr freigegeben wird, weil dieser andere Thread seinerseits auf den ersten Thread wartet. Beim geschachtelten Monitor ist die Situation etwas anders: der Thread, der wait() aufgerufen hat, wartet auf keine Sperre, sondern auf ein Signal, dass ein anderer Thread per notify() oder notifyAll() senden müßte. Es ist in der Regel notwendig, den Sourcecode manuell zu analysieren, um zu erkennen, welcher andere Thread potenziell in der Lage wäre, notify() oder notifyAll() auf der entsprechenden Bedingung aufzurufen. Es reicht nicht aus, lediglich die Sperren zu analysieren.

Und was macht man, wenn man bei der Fehleranalyse festgestellt hat, dass der Grund für den Fehler ein geschachtelter Monitor ist? Man wird versuchen, an beiden Stellen nur ein Mutex- bzw. Bedingungsobjekt zu nutzen. Schauen wir uns das Beispiel mit den Klassen A und B noch einmal an. Dort waren das Mutex- bzw. Bedingungsobjekt das this des Objekts vom Typ A und das this des Attributs b vom Typ B. Eine Lösung könnte so aussehen, dass wir das this des Objekts vom Typ A in beiden Fällen als Mutex- bzw. Bedingungsobjekt nutzten. Das bedeutet, das die Klasse B kein hardcodiertes Mutex- bzw. Bedingungsobjekt mehr nutzen darf, sondern dieses als Parameter im Konstruktor übergeben bekommt. Das Codefragment einer solchen Lösung sähe so aus:
 
public class A {
  private B b = new B(this);
  public synchronized void m1() {
     ...
    b.m3();
     ...
  }
  public synchronized void m2() {
     ...
    b.m4();
     ...
  }
}
public class B {
  Object myMutex;
  public B(Object mutex) {
    myMutex = mutex
  }
  public void m3() {
    synchronized (myMutex) {
       ...
       while (...)
         myMutex.wait();
       ...
    }
  }
  public void m4() {
    synchronized (myMutex) {
       ...
      if (...)
        myMutex.notifyAll();
       ...
    }
  }
}

In der Praxis ist es leider so, dass die Voraussetzungen für die Beseitigung des Nested-Monitor-Problems häufig gar nicht gegeben sind. Dazu müssten Klassen so aussehen wie die Klasse B in unserem Beispiel: sie dürften kein hardcodiertes Mutex- bzw. Bedingungsobjekt verwenden, sondern müßten wir oben gezeigt in der Lage sein, ein beliebiges von Außen übergebenes Mutex- bzw. Bedingungsobjekt benutzen. Schaut man sich aber existierende Klassen an, beispielsweise die Klassen des JDK an, die Synchronisation benutzen, so stellt man fest, dass sie hardcodierte Mutex- bzw. Bedingungsobjekte verwenden. Diese wenig flexible Implementierungstechnik wird im allgemeinen auch bei benutzerdefinierten Klassen verwendet, was letztendlich dazu führt, dass man in der Regel Zugriff auf den Sourcecode der beteiligten Klassen haben muss, um ein bereits identifiziertes Nested-Monitor-Problem zu korrigieren.
 

JDK 1.5 und das Nested-Monitor-Problem

Bisher haben wir uns die Problematik des geschachtelten Monitors aus der Pre-JDK-1.5-Perspektive angesehen. Das heißt, was wir bisher gesehen haben, gilt für die Java Multithread-Programmierung mir einem JDK vor 1.5. Mit dem JDK 1.5 ist es zu einer umfassenden Erweiterung des Multithread-APIs in Java gekommen. Die neuen expliziten Sperren haben wir ja bereits diskutiert (siehe / KRE3 /). An dieser Stelle wollen wir auf die Änderungen eingehen, die das Nested-Monitor-Problem in eine neue Perspektive rücken.

Ein Grund für die Erweiterung des Multithread-API im JDK 1.5 war unter anderem die Absicht, den Java API näher an einen wesentlichen Multithread-API Standard zu bringen, den es bei der Benutzung von C und C++ Programmen gibt: Posix Threads oder kurz P-Threads (siehe / POSIX /). Bei P-Threads ist es möglich, anders als bisher in Java, dass man mehrere Bedingungen mit einem Mutex verbindet. Das wird mit dem JDK 1.5 auch in Java möglich werden. Das heißt, es ist dann möglich, mehrere Bedingungsobjekte zu einem einzigen Mutex zu haben. Damit kann man dann auch einen BlockingIntStackWithTwoCondtions korrekt implementieren.

Bevor wir das machen, schauen wir uns die Details aus dem JDK 1.5 API an. Die Verbindung von mehreren Bedingungsobjekten mit einem Mutex läßt sich mit expliziten Sperren und Bedingugen erreichen. Die expliziten Sperren haben wir bereits in / KRE3 / besprochen.  Sehen wir uns an dieser Stelle die expliziten Bedingungen an. Im Package java.util.concurrent.locks, das neu im JDK 1.5 hinzukommen ist, gibt es ein Interface Condition, das die Funktionalität unterstützt, die man von einer Bedingung im Multithread-Kontext erwartet. Die Methoden des Interfaces Condition sind:

  • await() - das entspricht der Funktionalität des Object.wait() wie wir es in diesem und dem vorhergehenden Artikel diskutiert haben.
  • awaitNanos(long timeout) - das entspricht der Funktionalität des Object.wait() mit Timeout. Der Timeoutwert ist jetzt aber in Nanosekunden; bei Object.wait() sind es Millisekunden.
  • awaitUntil(Date deadline) - das entspricht der Funktionalität des Object.wait() mit Timeout, mit dem Unterschied,  dass ein Objekt vom Typ Date den Timeout-Zeitpunkt beschreibt
  • awaitUninterruptibly() - das entspricht der Funktionalität des Object.wait() ohne Timeout, mit dem Unterschied,  dass keine InterruptedException geworfen wird.
  • signal() und signalAll() - entsprechen von der Funktionalität her Object.notify()und Object.notifyAll().
Es sollte nicht verwunderlich sein, dass die Methoden in der neuen expliziten Bedingungsabstraktion Condition andere Namen haben als die alten Methoden in Object. Das geht ja auch gar nicht anders.  Condition ist zwar ein Interface, aber Objekte von konkreten Klassen, die das Interface Condition implementieren, sind natürlich von Object abgeleitet, wie alle Klassen in Java. Eine Instanz einer Klasse, die das Interface Condition implementiert, unterstützt deshalb sowohl die bedingungsspezifischen Methoden aus Object (wait(), notify(), notifyAll()) als auch die Methoden von Condition (await(), awaitNanos(), usw.). Von der Semantik her sind die jeweiligen Methoden sehr ähnlich, aber man beachte, dass sie sich auf ganz verschiedene Bedingungen bzw. Sperren beziehen.

Es gibt im neuen Package java.util.concurrent.locks allerdings keine Klasse, die das neuen Interface Condition implementiert.  Es stellt sich daher die Frage: woher bekommt man ein Bedingungsobjekt?  Offensichtlich nicht durch den Aufruf eines Konstruktors irgendeiner Klasse, die das Interface Condition implementiert. Stattdessen werden explizite Bedingungen über explizite Sperren erzeugt.  Da Bedingungsobjekte immer mit einem Mutex verbunden sein müssen, stellen die expliziten Sperren eine Factory-Methode für Bedingungsobjekte zur Verfügung.  Das Interface Lock im Package java.util.concurrent.locks hat eine Methode newCondition(), die ein Bedingungsobjekt liefert.

public Condition newCondition();
Von welchem Typ das Bedingungsobjekt ist, bleibt unbekannt und ist auch nicht wichtig,  Relevant ist nur, dass das gelieferte Bedingungsobjekt mit dem Mutex der expliziten Sperre verbunden ist, von der es erzeugt wurde.

Mit diesem Hilfsmittel und unter Berücksichtigung der vorhergehenden Überlegungen zum Nested-Monitor-Problem können wir den BlockingIntStackWithTwoCondtions_JDK_1_5 folgendermaßen implementieren:

public class BlockingIntStackWithTwoCondtions_JDK_1_5 {
 private Lock lock = new ReentrantLock();
 private Condition fullCon  = Locks.newCondition(lock);
 private Condition emptyCon = Locks.newCondition(lock);

 private final int[] array;
 private volatile int cnt = 0;

 public void push(int element) {
    lock.lock();
    try {
      while (cnt == size) {
        try { fullCon.await(); } catch (InterruptedException e) {...}
      }
      array[cnt++] = element;
      emptyCon.signal();
    } finally { lock.unlock(); }
 }

 public int pop() {
    lock.lock();
    try {
      while (cnt == 0) {
        try { emptyCon.await(); }
        catch (InterruptedException e) { ... }
      }
      int tmp = array[--cnt];
      fullCon.signal();
      return (tmp);
    } finally { lock.unlock(); }
 }

 public IntStack (int sz) { array = new int[sz]; }

 public int size() { return cnt; }

 public int capacity() { return (array.length); }
}

Mit dem JDK 1.5 ist es also nun möglich, jede logische Bedingungen durch ein eigenes Bedingungsobjekten zu repräsentieren. Der Vorteil einer 1:1-Abbildung von logischen Bedingungen auf Bedingungsobjekte ist, dass die logische Struktur des Programms deutlicher und expliziter dargestellt werden kann. Ein Blick auf den BlockingIntStack  vom Anfang dieses Artikels und auf den  BlockingIntStackWithTwoCondtions_JDK_1_5 oben sollte das deutlich machen: der Code ist bedeutend transparenter, wenn nur jeweils eine logische Bedingung über ein Bedingungsobjekt kommuniziert wird.

Trotz der neuen expliziten Bedingungsobjekte kann man natürlich immer noch Probleme in Form von geschachtelten Monitoren und Deadlocks produzieren. Es ist nach wie vor wichtig zu verstehen, wie viele Monitore verwendet werden bzw. dafür zu sorgen, dass immer nur ein Mutex gesperrt und entsperrt wird und verschiedene Bedingungsobjekte gemeinsam dasselbe Mutex verwenden müssen, um Probleme zu vermeiden. In unserer Implementierung oben tun wir das auch: beide Bedingungsobjekte fullCondition und emptyCondition sind an das eine Mutex der expliziten Sperre lock gekoppelt.  In anderen Situationen, wie wir sie am Beispiel der Klassen A und B diskutiert haben, ändert sich auch unter Verwendung von JDK 1.5 nichts.  Im Beispiel der Klassen A und B hatten wir ohnehin nur eine einzige Bedingung verwendet und das Problem lag in der Verwendung von zwei Mutexen.  Die neuen Abstraktionen des JKD 1.5 helfen in solchen Situationen nicht. Man muß immer noch analysieren und verstehen, welche Mutexe verwendet werden und dafür sorgen, dass nur ein Mutex gemeinsam verwendet wird. Wie das im Falle der Klassen A und B geht, haben wir ja schon gezeigt.

Zusammenfassung

In diesem Artikel haben wir uns das Problem geschachtelter Monitore angesehen. Es tritt auf, wenn mehrere Bedingungsobjekte und mehrere Mutexe verwendet werden.  Die Verwendung von mehreren Bedingungen ist eigentlich  durchaus erwünscht, weil sie zu einer klareren Programmstruktur führt, bei der jede logische Bedingung durch eine eigenes Bedingungsobjekt ausgedrückt wird. Mit dem klassischen Multithread-Support (vor JDK 1.5), bei dem jedem Bedingungsobjekt genau ein Mutex zugeordnet ist, führt die Verwendung von mehreren Bedingungsobjekten aber immer zum Nested-Monitor-Problem.  Das Nested-Monitor-Problem äußert sich durch den Stillstand der Applikation und ist unerwünscht.  Lösen lässt sich das Problem durch Abbildung von mehreren logischen Bedingungen auf ein einziges Bedingungsobjekt (unschöne Programmstruktur) oder mit dem JDK 1.5 durch die Verwendung von expliziten Condition-Objekten.

Randbemerkung "Hilfsmittel für die Deadlock-Analyse"
Für die Deadlock-Analyse stehen verschiedene Hilfsmittel zur Verfügung: Debugger, JVM-Output, Profiler und Management Beans. Darüber hinaus haben einige der in Java 1.5 neuen Klassen selbst bereits Methoden für das Monitoring. Wir wollen an dieser Stelle nicht erschöpfend auf all diese Hilfsmittel eingehen, sondern nur kurz darauf hinweisen, dass es sie gibt und dass sie u.U. nützlich sein können bei der Suche nach der Ursache für einen "Hänger".

Debugger
Die meisten Debugger können anzeigen, welche Threads in welchem Zustand sind und ob ein Thread auf einen Mutex wartet oder einen Mutex hält oder auf ein Signal an einer Condition wartet. Selbständige Deadlock-Analyse durch den Debugger ist ebenfalls möglich, wird aber nicht von allen Debuggern gemacht.

JVM-Output
Die virtuellen Maschinen haben in der Regel einen Mechanismus, mit dem man sich Diagnose-Output ausgeben lassen kann.  Bei der JVM von Sun kann man diesen Output erzeugen, wenn man während des Programmablaufs eine bestimmte Tastenkombination drückt: <ctrl> + <\> auf Linux und Solaris und <ctrl> + <break> auf Windows. Die virtuelle Maschine gibt dann einen Stack-Dump aus, in dem sich unter anderem Information über die Threads und deren Wartezustände findet. Hier ist ein Beispiel:

"pool-1-thread-1" prio=5 tid=0x008ef7d0 nid=0xdb8 runnable [0x0144f000..0x0144fae8]
        at booth.WorkLoad.work(WorkLoad.java:12)
        at booth.Shoppers$Shopper.checkout(Shoppers.java:55)
        at booth.Shoppers$Shopper.run(Shoppers.java:61)
        at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
        at java.lang.Thread.run(Unknown Source)
"main" prio=5 tid=0x00036c38 nid=0x980 in Object.wait() [0x0007f000..0x0007fc3c]
        at java.lang.Object.wait(Native Method)
        - waiting on <0x629e91b0> (a java.lang.Thread)
        at java.lang.Thread.join(Unknown Source)
        - locked <0x629e91b0> (a java.lang.Thread)
        at java.lang.Thread.join(Unknown Source)
        at booth.CheckoutSimulation.main(CheckoutSimulation.java:47)

Dabei ist die JVM auch in der Lage, Deadlocks zu finden und Information dazu anzuzeigen. Hier ist ein Beispiel:

Found one Java-level deadlock: 
============================= 
"Thread-4":
 waiting to lock monitor 0xb4868 (object 0xf203fdf8, a java.lang.Object) in JNI, which is held by "Thread-1" 
"Thread-1":
 waiting to lock monitor 0xb4910 (object 0xf203fe00, a java.lang.Object), which is held by "Thread-2"
"Thread-2":
 waiting to lock monitor 0xb4830 (object 0xf6112a60, a java.lang.String), which is held by "Thread-3" 
"Thread-3":
 waiting to lock monitor 0x119520 (JVMDI/JVMPI raw monitor), which is held by "Thread-4" 

Nested-Monitor-Situationen werden dabei allerdings nicht gefunden, weil dafür – wie schon erwähnt – semantisches Wissen erforderlich ist, das einem Tool nicht zur Verfügung steht.

Profiler
Im Rahmen der Java Platform Debugger Architecture (JPDA) können Diagnose-Agenten in die JVM eingehängt werden (siehe / JPDA /). Diese Agenten bedienen sich des JVMPI (Java Virtual Machine Profiler Interface) oder des  JVMTI (Java Virtual Machine Tool Interface). JVMPI existiert seit JDK 1.2 und wird in JDK 1.5 durch JVMTI abgelöst. Beide Schnittstellen erlauben umfangreiches Profiling, z.B. zum Speicher- oder CPU-Verbrauch, aber auch zu den Thread-Zuständen. 

Profiler-Agenten werden von verschiedenen JVM-Herstellern angeboten.  Der Standard-Profiler-Agent von Sun beispielsweise ist als HPROF bekannt (siehe / HPROF /). Man kann HPROF-Output über die Option –Xrunhprof anstoßen.  Um Information für die Threads zu bekommen würde etwa man folgende Option angeben:

java –Xrunhprof:monitor=y com.ibm.test.stresser
Die JVM schreibt dann Information über die Threadzustände in eine Ausgabedatei. Hier ein Auszug eines solchen Outputs:

THREAD START (obj=7c5e60, id = 1, name="Signal dispatcher", group="system") 
THREAD START (obj=7c6770, id = 2, name="Reference Handler", group="system") 
THREAD START (obj=7ca700, id = 3, name="Finalizer", group="system") 
THREAD START (obj=8427b0, id = 4, name="SymcJIT-LazyCompilation-PA", group="main") 
THREAD START (obj=7c0e70, id = 5, name="main", group="main") 
THREAD START (obj=87d910, id = 6, name="First", group="main") 
THREAD START (obj=87d3f0, id = 7, name="Second", group="main") 
THREAD START (obj=842710, id = 8, name="SymcJIT-LazyCompilation-0", group="main") 
THREAD END (id = 6) 
THREAD END (id = 5) 
THREAD START (obj=87e820, id = 9, name="Thread-0", group="main") 
THREAD END (id = 9)

MONITOR DUMP BEGIN
 THREAD 8, trace 1897, status: CW 
 THREAD 7, trace 1898, status: CW
 THREAD 6, trace 1899, status: MW 
 THREAD 9, trace 1888, status: CW 
 THREAD 5, trace 1900, status: CW 
 THREAD 4, trace 1901, status: CW 
 THREAD 3, trace 1902, status: CW 
 THREAD 2, trace 1903, status: R 
 MONITOR ThreadTest(8ecb50) 
  owner: thread 7, entry count: 2 
  waiting to be notified: thread 8 
 MONITOR SymantecJITCompilationThread(8b1e00) unowned 
  waiting to be notified: thread 5 
 RAW MONITOR "_Hprof CPU sampling lock"(0x8b2f70) 
  owner: thread 6, entry count: 1 
 RAW MONITOR "SymcJIT Lazy Queue Lock"(0x8b1d34) unowned 
  waiting to be notified: thread 9
MONITOR DUMP END 

Leider muss man hinzufügen, dass der HPROF-Monitor-Dump relativ unzuverlässig ist und nur gelegentlich funktioniert.  Es kann passieren, dass HPROF mit internen Fehlern abstürzt und gar keine brauchbare Information über die Threads produziert.  Das ist ein bekanntes Problem, von dem man nur hoffen kann, dass es in JDK 1.5 mit Umstellung auf die neue JVMTI-Schnittstelle behoben wird.

Management Beans
Schließlich wären noch die Java Management Beans (MXBeans) zu erwähnen. Sie sind neu in JDK 1.5 hinzugekommen (siehe / JSR163 / und / JSR174 /) und bieten eine Schnittstelle für den programmatischen Zugang zu Profiling- und Monitoring-Informationen der JVM.  Für die Deadlock-Analyse ist die ThreadMXBean von Interesse; es gibt daneben eine ganze Reihe anderer MXBeans im Package java.lang.management.

ThreadMXBean. ist ein Interface. In der JVM existiert genau eine Instanz der ThreadMXBean.  Zugang zur MXBean holt man sich über eine Factory mit dem Aufruf ManagementFactory.getThreadMXBean().  Die ThreadMXBean liefert dann Informationen über die Threads und deren Zustände. Das funktioniert über Methoden wie long[] getAllThreadIds(), getThreadInfo (long id) und ThreadState getThreadState(long id).  Man bekommt Informationen über die Threads, wie Thread ID und Threadname, Threadzustand (New, Running, Blocked on entering or reentering a synchronization block, Waiting for notification, Timed waiting for notification, Sleeping, Terminated, Other), den Thread, der das Objekt blockiert, auf dessen Mutex der Thread wartet, usw. Man kann sogar eine Deadlock-Analyse über die Methode long[] findMonitorDeadlockedThreads() anstoßen. 

Monitoring- und Debug-Methoden in ReentrantLock, ReentrantReadWriteLock und Semaphore
Bei allen bisher genannten Hilfsmittel muss man leider anmerken, dass sie nur mit dem alten impliziten Mutex und den impliziten Conditions funktionieren.  Die im JDK 1.5 neuen expliziten Locks und Conditions sind nicht integriert, nicht einmal in die in JDK 1.5 neuen Management Beans.  Stattdessen habe die neuen Abstraktionen selbst ein Monitoring-Interface.
Die Klasse ReentrantLock bietet beispielsweise Methoden, die Informationen darüber geben, ob der Mutex gesperrt ist, wie oft er angefordert wurde und von welchem Thread er gehalten wird.  Die Methoden sehen so aus:

public boolean isLocked()
public boolean isHeldByCurrentThread()
public int getHoldCount()
protected Thread getOwner()
Daneben werden Informationen darüber geliefert, welche Threads darauf warten, den Mutex zu sperren.
public boolean hasQueuedThreads()
public int getQueueLength()
public boolean hasQueuedThread(Thread thread)
protected Collection<Thread> getQueuedThreads()
Die Klassen ReentrantReadWriteLock und Semaphore haben ähnliche Methoden. All diese Monitoring- und Debug-Methoden ermöglichen den programmatischen Zugang zu den Information, die bei der Suche nach einem Deadlock helfen können.

Literaturverweise

 
/KRE1/ Multithread Support in Java, Teil 1: Grundlagen der Multithread-Programmierung
Klaus Kreft & Angelika Langer
JavaSPEKTRUM, Januar 2004
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/12.MT-Basics/12.MT-Basics.html
/KRE2/ Multithread Support in Java, Teil 2: Details zum synchronized Schlüsselwort
Klaus Kreft & Angelika Langer
JavaSPEKTRUM, März 2004
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/13.synchronized/13.synchronized.html
/KRE3/ Multithread Support in Java, Teil 3: Erweiterungen für das Sperren von Threads im JDK 1.5
Klaus Kreft & Angelika Langer
JavaSPEKTRUM, Mai 2004
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/14.ExplicitLocks/14.ExplicitLocks.html
/KRE4/ Multithread Support in Java, Teil 4: Synchronisierung mit Hilfe von wait() und notify()/notifyAll()
Klaus Kreft & Angelika Langer
JavaSPEKTRUM, Juli 2004
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/15.WaitNotify/15.WaitNotify.html
/POSIX/ POSIX-Thread-Standard: IEEE POSIX 1003.1c-1995 (also known as the ISO/IEC 9945-1:1996) 
part of IEEE/ANSI Std 1003.1, 1996 Edition
Information Technology —  Portable Operating System Interface (POSIX®)
Part 1: System Application: Program Interface (API) [C Language]
ISBN 1-55937-573-6
URL: http://standards.ieee.org/
/JSR166/  Java Specification Request JSR 166 - Concurrency Utilities
URL: http://www.jcp.org/en/jsr/detail?id=166
Concurrency JSR-166 Interest Site
URL: http://gee.cs.oswego.edu/dl/concurrency-interest/index.html
Overview of package util.concurrent Release 1.3.2.
URL: http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html
/JSR163/ Java Specification Request JSR 163: Java TM Platform Profiling Architecture
URL: http://jcp.org/en/jsr/detail?id=163
/JSR174/  Java Specification Request JSR 174: Monitoring and Management Specification for the Java TM Virtual Machine
URL: http://jcp.org/en/jsr/detail?id=174
/JPDA/ Java Platform Debugger Architecture (JPDA) 
URL: http://java.sun.com/products/jpda/index.jsp
/HPROF/  The HPROF Profiler Agent
URL: http://java.sun.com/j2se/1.4.2/docs/guide/jvmpi/jvmpi.html#hprof

 
 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Concurrent Java - Java Multithread Programming
4 day seminar ( open enrollment and on-site)
 
  © Copyright 1995-2008 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/16.NestedMonitorProblem/16.NestedMonitorProblem.html  last update: 26 Nov 2008