2.8 Synchronisation

In Java gibt es, wie in Unix oder Windows NT, die Möglichkeit, Threads zu synchronisieren, damit sie, obwohl sie normalerweise unabhängig von einander ausgeführt werden, aufeinander abgestimmt werden können.

 

2.8.1 Synchronisation durch Priorität

Durch die Priorität eines Threads wird in Java die Reihenfolge festgelegt, in der die Threads einer Applikation oder eines Applets abgearbeitet werden (siehe Kapitel 2.4). Eine Synchronisation zwischen den Prozessen findet aber nicht statt. Deshalb darf man streng genommen hier nicht von einer Synchronisation sprechen sondern nur von der Bevorzugung eines bestimmten Threads. Um die Priorität eines Threads zu verändern verwendet man die Methode setPriority(). Die Priorität kann von 1 - 10 eingestellt werden. Es kann zu Schwierigkeiten kommt, wenn man die Priorität eines rechenintensieven Threads zu hoch setzt (z.B. auf 10). In so einem Fall werden die System-Threads eventuell vernachlässigt, so daß das System nicht mehr einwandfrei läuft. Bei einem Applet kann die Priorität nicht beliebig erhöht werden. Das System verhindert, das ein Applet durch seine Threads das System blockiert.

Mit der Methode getPriority() erhält man die aktuelle Priorität des Threads.

 

2.8.2 Synchronisation durch Monitore

Das Konzept des Monitors paßt von der Konstruktion her sehr gut zur objektorientierten Programmierung. Unter einem Monitor kann man sich ein Objekt vorstellen, das bestimmte Attribute in sich einschließt, auf die nur über definierte Zugangsmethoden zugegriffen werden kann. Der Monitor ist besetzt, sobald eine Zugangsmethode ausgeführt wird. Zu jeder Zugangsmethode verwaltet der Monitor eine Warteschlange, in die Threads eingereiht werden, die eine solche Methode aufrufen, während der Monitor besetzt ist.

Durch das Einfügen des Modifier synchronized in der Deklaration der Methode erhält man so einen Monitor. Sobald eine mit syncronized deklarierte Methode von einem Thread ausgeführt wird, kann kein anderer Thread eine synchronized Zugangsmethode von diesem Objekt mehr ausführen.

Das folgende Beispiel soll schemenhaft zeigen, wie ein solcher Monitor angewendet werden kann. Zwei Threads rufen hierbei mit unterschiedlicher Geschwindigkeit die Methode put() der Klasse Ausgabe auf. Obwohl diese Methode absichtlich zeitaufwendig gestaltet wurde wird sie zu Ende geführt obwohl ein anderer Thread sie zwischenzeitlich ebenfalls aufgerufen hat. (Das ausführliche Beispiel mit der Bildschirmausgabe befindet sich im 6. Kapitel dieser Diplomarbeit).

 

Beispiel für die Synchronisierung einer Methode:


class Ausgabe {
...         // Übergabe eines Strings und der Nummer des Threads 
public synchronized void put (String ausgabe, int nummer) {
...
  System.out.println ("Thread Nummmer"+nummer+"beginnt mit der
                      Methode");
  System.out.println (ausgabe);         // Ausgabe des Strings
  System.out.println ("Thread Nummmer"+nummer+"beendet die 
                      Methode");
 }
}

 

2.8.3 Synchronisation durch Sperr-Objekte

Benötigen die Threads mehrere Ressourcen gleichzeitig, so kann es schnell zu einem Deadlock kommen wenn synchronized auf Methoden angewendet wird. Bei einem Java Programm kann dies geschehen, wenn sich zwei Monitore in den Zugangsmethoden gegenseitig aufrufen.

In diesem Fall kann es vorkommen, daß ein Thread den Monitor A betritt und ein anderer den Monitor B. Wenn nun in der Zugangsmethode von A eine Zugangsmethode von B aufgerufen wird und in B eine von A, so wartet der erste Thread darauf, daß der zweite den Monitor B verläßt, dieser wartet jedoch darauf, daß der erste den Monitor A verläßt.

Benötigt ein oder mehrere Threads mehrere Ressourcen gleichzeitig kann der Zugriff auf diese Ressourcen durch das Schlüsselwort synchronized auf eine zweite Art verwendet werden.

Beispiel für die Synchronisation mit einem Sperr-Objekt:


class SteuerObj {}

class MeineKlasse {
 SteuerObj Objekt1 = new SteuerObj() ;
 ....
 public void Methode() {
  synchronized(Objekt1) {                    // kritischer Bereich
  ** Programmcode der synchronisiert werden soll **		
 }
}

Wird synchronized wie im Beispiel gezeigt eingesetzt, erfolgt die Synchronisierung anhand eines Sperr-Objektes. In diesem Beispiel ist das Sperr-Objekt "Objekt1".

Bei dieser Art der Synchronisation kann ein Teil einer Methode nicht synchronisiert und ein anderer Teil synchronisiert ausgeführt werden. Dies hat den Vorteil, daß der synchronisierte Teil so kurz wie möglich gehalten werden kann, egal wie lang die eigentliche Methode ist.

Ein weiterer Vorteil besteht darin, daß wenn es nötig ist diese synchronized Blöcke geschachtelt werden können.

Beispiel für geschachtelte Sperr-Objekte:

synchronized(Objekt1) {
 synchronized(Objekt2) {
  synchronized(Objekt3) {          // kritischer Bereich
    ** Programmcode der synchronisiert werden soll **	
  }
 }
}

Bei einer geschachtelten Synchronisation muß allerdings streng darauf geachtet werden, daß die Threads diese Schachtelung in der gleichen Reihenfolge durchlaufen, da sonst ein Deadlock entsteht.

Beispiel wie ein Deadlock entstehen kann:

// Thread 1 
synchronized(Objekt1) {
 synchronized(Objekt2) {
	.....
 }
}

// Thread 2
synchronized(Objekt2) {
 synchronized(Objekt1) {
	.....
 }
}

Wenn bei diesem Beispiel Thread 1 das Objekt1 belegt und Thread 2 direkt danach Objekt2 in seinen Besitz nimmt entsteht ein Deadlock, da beide Threads jeweils das Sperr-Objekt des anderen Threads benötigen um ihren Programmcode weiter auszuführen.

 

2.8.4 Synchronisation durch Klassenbezogene-Sperre

Wenn man sichergehen muß, daß ein kritischer Bereich für sämtliche Threads synchronisiert ist muß dieses Synchronisations Objekt das definitiv einzige Exemplar seiner Klasse sein. In so einem Fall kann man das Class-Objekt verwenden.

Das Class-Objekt einer Klasse ist einmalig in einer Virtual Machine (insofern ist es auch gerechtfertigt, von »dem« Class-Objekt zu sprechen). Daher können mit diesem Objekt alle Threads einer Virtual Machine synchronisiert werden, auch dann, wenn sie unterschiedliche Exemplare der Monitor-Klassen halten oder wenn von der Monitor-Klasse gar keine Exemplare erzeugt werden können, weil sie ausschließlich statische Zugangsmethoden definieren.

Diese Art der Synchronisation kann z.B. bei der Arbeit mit Applets eingesetzt werden. Obwohl sie völlig getrennte Programme sind laufen sie in der selben Virtual Machine im Browser. Es ist dabei unwichtig ob sich die Applets in verschiedenen Dokumenten vom gleichen Host befinden und in getrennten Fenster angezeigt werden.

In dem folgendem Beispiel synchronisieren sich zwei Applets mit Hilfe der Klasse StaticMonitor indem die Methoden put() und get() jeweils von einem Applet aufgerufen werden:

public class StaticMonitor { 
 static int buffer; 
 static boolean empty = true; 

 public synchronized static void put(int data) { 
  try {                                     
   Class classLock = StaticMonitor.class;   
  if (!empty                        // Class-Objekt wird ermittelt
    classLock.wait();               // Wenn der Speicher voll ist
   buffer = data;                   // wird gewartet.
   empty = false;         	    // Speicher wird auf leer gesetzt
   classLock.notify();              // Ein eventuell wartender Thread 
  }                                 // wird benachrichtigt und kann jetzt
  catch(InterruptedException e) { }    // weiter arbeiten 
 } 

 public synchronized static int get() { 
  try { 
   Class classLock = StaticMonitor.class;   // Class-Objekt wird ermittelt
   if (empty)                               // Wenn der Speicher leer ist
    classLock.wait();                       // wird gewartet.
   System.out.println( "Abgeholt: "+ buffer ); 
   empty = true;                     // Speicher wird auf voll gesetzt.
   classLock.notify();               // Ein eventuell wartender Thread
  }                                  // wird benachrichtigt und kann jetzt
  catch(InterruptedException e) {}   // weiter arbeiten.
  return buffer; 
 } 
} 

Wie in diesem Beispiel zu sehen ist wird zuerst das Class-Objekt in beiden Methoden ermittelt. Die Synchronisation findet dann über dieses Objekt mittels wait() und notify() statt. Dadurch, daß die beiden Zugangsmethoden als static vereinbart sind wird das klassenbezogene Sperren erreicht. Vom grundsätzlichem Aufbau her stimmen die Zugangsmethoden mit denen, die bei der Synchronisation mit Ereignissen verwendet werden, überein.

Methoden für das klassenbezogene Sperren und der Synchronisation mit Ereignissen:

wait

public final void wait()
Wartet auf den Eintritt eines Ereignisses. Diese Methode kann nur aus synchronisierten Methoden heraus aufgerufen werden.

notify

public final native void notify()
Benachrichtigt einen wartenden Thread von einem Ereignis. Kann nur in synchronisierten Methoden aufgerufen werden.

 

2.8.5 Synchronisation durch Ereignisse

Bei der Synchronisation mit Ereignissen besitzt der Programmierer eine größere Kontrolle über die Abläufe im Zusammenhang mit Monitoren. So kann der Programmierer z.B. den Wert eines Datenelementes vorgeben, der vorhanden sein muß damit ein Thread den Monitor erhält. Ist dies nicht der Fall muß der Thread warten bis das der richtige Wert bzw. das Ereignis eintritt. In dem Beispielprogramm zu dieser Synchronisationsart wurde die Anzahl der Bildschirmausgaben, die jeder Thread ohne Unterbrechung ausführt, als Ereignis verwendet (vollständiges Programm und Bildschirmausgabe ist in Kapitel 6 abgedruckt).

Beispiel für die Synchronisation mit Ereignissen:

class Ausgabe {

 private int anzahl=0;
 private int soll=3;             // Anzahl der Ausgaben
 boolean th1 = true;             // Variable die angibt, welcher
                                 // Thread den Monitor erhält.

   // Synchronisierte Methode, die von Thread 1 aufgerufen wird.
 public synchronized void put1 (String ausgabe) {  			
  if (!th1)                        // Wenn th1 == false
   try {wait();}                   // muß Thread 1 warten.
  catch(InterruptedException e) {}		
  this.buffer=ausgabe;
  System.out.println(buffer);     // Hier erfolgt die Ausgabe auf 
                                  // dem Bildschirm.
  anzahl=anzahl+1;     // Anzahl der Ausgaben wird heraufgesetzt.
  if (anzahl>=soll)    // Anzahl der Ausgaben wird mit dem 
  {                    // Sollwert verglichen.
   anzahl=0;           // Anzahl der Ausgaben wird auf 0 gesetzt.
   th1=false;          // Variable für den Monitor wird umgestellt
   notify();           // Eventuell wartender Thread erhält die 
  }                    // Benachrichtigung, daß er
 }                     // jetzt weiter arbeiten kann.
}
 
     // Synchronisierte Methode, die von Thread 2 aufgerufen wird.
 public synchronized void put2 (String ausgabe) {
  if (th1)	                              // Wenn th1 == true
   try {wait();}                              // muß Thread 2 warten.
   catch(InterruptedException e) {} 
  this.buffer=ausgabe;
  System.out.println(buffer);         // Hier erfolgt die Ausgabe 
                                      // auf dem Bildschirm.
  anzahl=anzahl+1;     // Anzahl der Ausgaben wird heraufgesetzt
  if (anzahl>=soll)    // Anzahl der Ausgaben wird mit dem 
                       // Sollwert verglichen.
  {
   anzahl=0;           // Anzahl der Ausgaben wird auf 0 gesetzt.
   th1=true;           // Variable für den Monitor wird umgestellt
   notify();           // Eventuell wartender Thread erhält die 
  }	                 // Benachrichtigung, daß er jetzt 
 }                     // weiter arbeiten kann.
}

 

2.8.6 Synchronisation auf das Ende von anderen Threads

In Java definiert die Klasse Thread die Methode join(). Diese Methode kann verwendet werden um einen Thread auf das Ende eines anderen Threads zu synchronisieren.

Mit join() können auf einfache Weise Verarbeitungsketten implementiert werden, bei denen ein Thread auf die Fertigstellung eines Zwischenergebnisses warten muß, welches wiederum von anderen Threads berechnet wird.

Mit dem Aufruf thread2.join() wartet der aufrufende Thread bis der Thread "thread2" seine Ausführung beendet hat.

Es gibt bei der Verwendung von join() einige Details, die beachtet werden müssen:

Beispiel für die Synchronisation auf das Ende eines anderen Threads:

class CountingThread extends Thread {

               // Modifizierte start()-Methode. Es wird der 
               // Thread übergeben, auf den gewartet werden soll.
 public void start(Thread thread) {
 
  try{thread.join();}
  catch(InterruptedException e) {}	

  super.start();      // Ist der Thread, auf den gewartet wird zu 
 }                    // Ende wird der wartende Thread gestartet.

 public void run(){
  ** Programmcode, der als Thread ausgeführt werden soll **
 }
}

class MeinThread2 extends Thread {   // "Normaler" Thread, auf den
                                     // gewartet werden soll.
 public void run() {
  ** Programmcode, der als Thread ausgeführt werden soll **
 }
}

class AufEndeThread {                           // Main-Methode
 ...
 CountingThread thread1 = new CountingThread(); // Erzeugen der
 MeinThread2 thread2 = new MeinThread2();       // beiden Threads

 thread2.start();           // Thread, auf den gewartet 
                            // werden soll wird zuerst gestartet.
 thread1.start(thread2);    // Thread, der warten soll wird 
                            // gestartet. Ihm wird der Thread
}                           // "thread2" übergeben auf den er 
                            // warten soll.