Skip to content

7. Ausführungsthreads

7.1. Einführung

Wenn eine Anwendung gestartet wird, wird sie in einem Ausführungsstrom ausgeführt, der als Thread bezeichnet wird. Die Klasse, die ein thread modelliert, ist die Klasse java.lang.Thread, deren Eigenschaften und Methoden im Folgenden aufgeführt sind:

currentThread()
gibt den aktuell ausgeführten Thread zurück
setName()
legt den Namen des Threads fest
getName()
Name des Threads
isAlive()
Gibt an, ob der Thread aktiv ist (true) oder nicht (false)
start()
Startet die Ausführung eines Threads
run()
Methode, die automatisch ausgeführt wird, nachdem die vorherige start-Methode ausgeführt wurde
sleep(n)
unterbricht die Ausführung eines Threads für n Millisekunden
join()
blockierende Operation – wartet auf das Ende des Threads, um zur nächsten Anweisung überzugehen

Die am häufigsten verwendeten Konstruktoren sind die folgenden:

Thread()
Erstellt eine Referenz auf eine asynchrone Aufgabe. Diese ist noch inaktiv. Die erstellte Aufgabe muss über die Methode `run` verfügen: Meistens wird eine von Thread abgeleitete Klasse verwendet.
Thread(Runnable object)
Das Gleiche gilt, allerdings ist es das als Parameter übergebene Objekt Runnable, das die Methode run implementiert.

Sehen wir uns eine erste Anwendung an, die das Vorhandensein eines Hauptausführungsthreads verdeutlicht, in dem die Funktion main einer Klasse ausgeführt wird:

// Verwendung von Threads

import java.io.*;
import java.util.*;

public class thread1{
    public static void main(String[] arg)throws Exception {
         // aktuellen Thread initialisieren
        Thread main=Thread.currentThread();
         // Anzeige
        System.out.println("Thread courant : " + main.getName());
         // Name wird geändert
        main.setName("myMainThread");
         // Überprüfung
        System.out.println("Thread courant : " + main.getName());

         // Endlosschleife
        while(true){
        // Zeit abrufen
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
             // Anzeige
            System.out.println(main.getName() + " : " +H);
             // vorübergehende Unterbrechung
            Thread.sleep(1000);
        }//while
    }//main
}//Klasse

Die Bildschirmausgabe:


Thread courant : main
Thread courant : myMainThread
myMainThread : 15:34:9
myMainThread : 15:34:10
myMainThread : 15:34:11
myMainThread : 15:34:12
Terminer le programme de commandes (O/N) ? o

Das vorstehende Beispiel veranschaulicht folgende Punkte:

  • Die Funktion main wird ordnungsgemäß in einem Thread ausgeführt
  • man hat über Thread.currentThread() Zugriff auf die Eigenschaften dieses Threads
  • die Rolle der Methode sleep. Hier versetzt sich der Thread, der main ausführt, zwischen zwei Anzeigen regelmäßig für 1 Sekunde in den Ruhezustand.

7.2. Erstellung von Ausführungsthreads

Es gibt Anwendungen, in denen Codeabschnitte „gleichzeitig“ in verschiedenen Ausführungsthreads ausgeführt werden. Wenn man sagt, dass threads gleichzeitig ausgeführt werden, handelt es sich oft um eine sprachliche Ungenauigkeit. Verfügt der Rechner nur über einen Prozessor – was nach wie vor häufig der Fall ist –, teilen sich die threads diesen Prozessor: Sie haben abwechselnd für einen kurzen Moment (einige Millisekunden) Zugriff darauf. Dies erzeugt die Illusion einer parallelen Ausführung. Die einem thread zugewiesene Zeit hängt von verschiedenen Faktoren ab, darunter seine Priorität, die einen Standardwert hat, aber auch programmgesteuert festgelegt werden kann. Wenn ein thread über den Prozessor verfügt, nutzt er diesen normalerweise während der gesamten ihm zugewiesenen Zeit. Er kann ihn jedoch vorzeitig freigeben:

  • indem es auf ein Ereignis wartet (wait, join)
  • indem er für eine bestimmte Zeit in den Ruhezustand wechselt (sleep)
  • Ein Thread T kann auf verschiedene Arten erstellt werden
    • durch Ableitung der Klasse Thread und Neudefinition der darin enthaltenen Methode run.
    • durch Implementierung der Schnittstelle Runnable in einer Klasse und Verwendung des Konstruktors new Thread(Runnable). Runnable ist eine Schnittstelle, die nur eine einzige Methode definiert: public void run(). Das Argument des oben genannten Konstruktors ist daher jede Instanz einer Klasse, die diese Methode run implementiert.

Im folgenden Beispiel werden die Threads mithilfe einer anonymen Klasse erstellt, die von der Klasse Thread abgeleitet ist:

            // Thread i wird erstellt
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//Aufgaben definieren[i]

Die Methode run verweist hier lediglich auf die Methode affiche.

  • Die Ausführung des Threads T wird durch T.start() gestartet: Diese Methode gehört zur Klasse „Thread“ und führt eine Reihe von Initialisierungen durch, bevor sie automatisch die Methode run des Threads oder der Schnittstelle Runnable aufruft. Das Programm, das die Anweisung T.start() ausführt, wartet nicht auf das Ende der Aufgabe T: Es springt sofort zur nächsten Anweisung. Es laufen also zwei Aufgaben parallel ab. Oft müssen sie miteinander kommunizieren können, um zu erfahren, wie weit die gemeinsam auszuführende Arbeit fortgeschritten ist. Dies ist das Problem der Thread-Synchronisation.
  • Einmal gestartet, läuft der Thread thread autonom ab. Er wird beendet, sobald die von ihm ausgeführte Funktion run ihre Arbeit beendet hat.
  • Man kann mit T.join() auf das Ende der Ausführung des Threads T warten. Hierbei handelt es sich um eine blockierende Anweisung: Das Programm, das sie ausführt, wird blockiert, bis die Aufgabe T ihre Arbeit beendet hat. Dies ist ebenfalls eine Möglichkeit der Synchronisation.

Betrachten wir das folgende Programm:

// Verwendung von Threads

import java.io.*;
import java.util.*;

public class thread2{
    public static void main(String[] arg) {
         // Initialisierung des aktuellen Threads
        Thread main=Thread.currentThread();
         // dem aktuellen Thread wird ein Name zugewiesen
        main.setName("myMainThread");
         // Start der Hauptfunktion
        System.out.println("début du thread " +main.getName());

         // Erstellung von Ausführungsthreads
        Thread[] tâches=new Thread[5];
        for(int i=0;i<tâches.length;i++){
             // Thread i wird erstellt
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//Aufgaben definieren[i]
             // Der Name des Threads wird festgelegt
            tâches[i].setName(""+i);
             // Start der Ausführung von Thread i
            tâches[i].start();
        }//for

         // Ende von main
        System.out.println("fin du thread " +main.getName());
    }//Main

    public static void affiche() {
         // die Uhrzeit wird abgerufen
    Calendar calendrier=Calendar.getInstance();
    String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // Anzeige: Beginn der Ausführung
        System.out.println("Début d'exécution de la méthode affiche dans le Thread " + 
        Thread.currentThread().getName()+ " : " + H);
         // 1 Sekunde lang in den Ruhezustand versetzen
        try{
        Thread.sleep(1000);
    }catch (Exception ex){}
     // Zeit abrufen
    calendrier=Calendar.getInstance();    
    H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // Anzeige des Ausführungsendes
        System.out.println("Fin d'exécution de la méthode affiche dans le Thread " 
    +Thread.currentThread().getName()+ " : " + H);
    }// Anzeige
}//Klasse

Der Hauptthread, der die Funktion main ausführt, erstellt fünf weitere Threads, die die statische Methode affiche ausführen. Die Ergebnisse lauten wie folgt:


début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 15:48:3
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 1 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 2 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 3 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 4 : 15:48:3
Fin d'exécution de la méthode affiche dans le Thread 0 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 1 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 2 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 3 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 4 : 15:48:4

Diese Ergebnisse sind sehr aufschlussreich:

  • Zunächst einmal lässt sich erkennen, dass der Start eines Threads nicht blockierend ist. Die Methode main hat die Ausführung von 5 Threads parallel gestartet und ihre eigene Ausführung vor diesen abgeschlossen. Die Operation
             // Start der Ausführung von Thread i
            tâches[i].start();

löst die Ausführung des Threads „tâches[i]“ aus, doch danach wird die Ausführung sofort mit der folgenden Anweisung fortgesetzt, ohne auf das Ende der Ausführung des Threads zu warten.

  • Alle erstellten Threads müssen die Methode affiche ausführen. Die Ausführungsreihenfolge ist unvorhersehbar. Auch wenn im Beispiel die Ausführungsreihenfolge der Reihenfolge des Starts der Threads zu folgen scheint, lassen sich daraus keine allgemeinen Schlussfolgerungen ziehen. Das Betriebssystem verfügt hier über 6 Threads und einen Prozessor. Es verteilt die Prozessorleistung auf diese 6 Threads nach eigenen Regeln.
  • In den Ergebnissen ist eine Auswirkung der Methode sleep zu erkennen. Im Beispiel ist es Thread 0, der als erster die Methode affiche ausführt. Die Meldung zum Start der Ausführung wird angezeigt, anschließend führt er die Methode sleep aus, die ihn für 1 Sekunde anhält. Er verliert daraufhin den Prozessor, der somit für einen anderen Thread verfügbar wird. Das Beispiel zeigt, dass Thread 1 diesen erhält. Thread 1 durchläuft denselben Ablauf wie die anderen Threads. Sobald die einsekündige Wartezeit von Thread 0 abgelaufen ist, kann dessen Ausführung fortgesetzt werden. Das System weist ihm den Prozessor zu, und er kann die Ausführung der Methode affiche abschließen.

Ändern wir unser Programm so, dass die Methode main mit den folgenden Anweisungen beendet wird:

         // Ende der Hauptschleife
        System.out.println("fin du thread " +main.getName());
     // Anwendung wird beendet
    System.exit(0);

Die Ausführung des neuen Programms ergibt:


début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 1 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 2 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 3 : 16:5:45
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 4 : 16:5:45

Sobald die Methode main die Anweisung ausführt:

    System.exit(0);

beendet sie alle Threads der Anwendung und nicht nur den Thread main. Die Methode main möchte möglicherweise abwarten, bis die von ihr erstellten Threads ihre Ausführung beendet haben, bevor sie selbst beendet wird. Dies kann mit der Methode join der Klasse Thread erfolgen:

     // Warten auf alle Threads
        for(int i=0;i<tâches.length;i++){
            // Warten auf Thread i
            tâches[i].join();
    }//for  

         // Ende der Hauptschleife
        System.out.println("fin du thread " +main.getName());
     // Anwendung beendet
    System.exit(0);

Daraus ergeben sich folgende Ergebnisse:

début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 1 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 2 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 3 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 4 : 16:11:9
Fin d'exécution de la méthode affiche dans le Thread 0 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 1 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 2 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 3 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 4 : 16:11:10
fin du thread myMainThread

7.3. Bedeutung von Threads

Nachdem wir nun die Existenz eines Standard-Threads aufgezeigt haben – nämlich den, der die Methode Main ausführt – und wissen, wie man weitere Threads erstellt, wollen wir uns nun mit dem Nutzen von Threads für uns befassen und erläutern, warum wir sie hier vorstellen. Es gibt eine Art von Anwendungen, die sich gut für den Einsatz von Threads eignen: Client-Server-Anwendungen im Internet. In einer solchen Anwendung beantwortet ein Server auf einem Rechner S1 Anfragen von Clients auf entfernten Rechnern C1, C2, …, Cn.

Image

Wir nutzen täglich Internetanwendungen, die diesem Schema entsprechen: Webdienste, E-Mail, Forenbesuche, Dateiübertragungen … In dem obigen Schema muss der Server S1 die Clients Ci gleichzeitig bedienen. Nehmen wir das Beispiel eines Servers FTP (File Transfer Protocol), der Dateien an seine Clients überträgt. Wir wissen, dass eine Dateiübertragung manchmal mehrere Stunden dauern kann. Es kommt natürlich nicht in Frage, dass ein einzelner Client den Server über einen so langen Zeitraum allein beansprucht. Üblicherweise erstellt der Server so viele Ausführungs-Threads, wie es Clients gibt. Jeder Thread ist dann dafür zuständig, sich um einen bestimmten Client zu kümmern. Da der Prozessor zyklisch zwischen allen aktiven Threads des Rechners aufgeteilt wird, verbringt der Server ein wenig Zeit mit jedem Client und gewährleistet so die Parallelität des Dienstes.

Image

7.4. Eine grafische Uhr

Betrachten wir die folgende Anwendung, die ein Fenster mit einer Uhr und einer Schaltfläche zum Anhalten oder Neustarten der Uhr anzeigt:

Damit die Uhr läuft, muss ein Prozess dafür sorgen, dass die Zeit jede Sekunde aktualisiert wird. Gleichzeitig müssen die Ereignisse im Fenster überwacht werden: Wenn der Benutzer auf die Schaltfläche „Stoppen“ klickt, muss die Uhr angehalten werden. Hier haben wir es mit zwei parallelen und asynchronen Aufgaben zu tun: Der Benutzer kann jederzeit klicken.

Betrachten wir den Moment, in dem die Uhr noch nicht gestartet wurde und der Benutzer auf die Schaltfläche „Starten“ klickt. Hier handelt es sich um ein klassisches Ereignis, und man könnte meinen, dass eine Methode des Threads, in dem das Fenster ausgeführt wird, die Uhr dann verwalten könnte. Allerdings hört der Thread der grafischen Anwendung nicht mehr auf Ereignisse der Benutzeroberfläche, sobald eine Methode der grafischen Anwendung ausgeführt wird. Diese Ereignisse treten auf und werden in eine Warteschlange gestellt, um von der Anwendung verarbeitet zu werden, sobald die aktuell ausgeführte Methode beendet ist. In unserem Uhrenbeispiel wird die Methode ständig ausgeführt, da sie nur durch einen Klick auf die Schaltfläche „Stopp“ beendet werden kann. Dieses Ereignis wird jedoch erst verarbeitet, wenn die Methode beendet ist. Wir drehen uns im Kreis.

Die Lösung für dieses Problem wäre, dass beim Klicken des Benutzers auf die Schaltfläche „Starten“ eine Aufgabe zur Verwaltung der Uhr gestartet wird, die Anwendung jedoch weiterhin die im Fenster auftretenden Ereignisse abhören kann. Man hätte dann zwei separate Aufgaben, die parallel ausgeführt würden:

  • Verwaltung der Uhr
  • Abhören von Ereignissen im Fenster

Kommen wir zurück zu unserer grafischen Uhr:

Nr.
Typ
Name
Rolle
1
JTextField (Bearbeitbar=false)
txtHorloge
zeigt die Uhrzeit an
2
JButton
btnGoStop
stoppt oder startet die Uhr

Der Quellcode der mit JBuilder erstellten Anwendung lautet wie folgt:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;

public class interfaceHorloge extends JFrame {
  JPanel contentPane;
  JTextField txtHorloge = new JTextField();
  JButton btnGoStop = new JButton();

  // Instanzattribute
  boolean finHorloge=true;

   //Rahmen erstellen
  public interfaceHorloge() {
    enableEvents(AWTEvent.WINDOW_EVENT_MASK);
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

  private void runHorloge(){
    // Schleife, bis wir den Befehl zum Beenden erhalten
    while( ! finHorloge){
       // Die Uhrzeit abrufen
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // Anzeige im Feld T
      txtHorloge.setText(H);
       // eine Sekunde warten
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // Beenden mit Fehler
        System.exit(1);
      }//try
    }// while
  }// runHorloge

   //Komponente initialisieren
  private void jbInit() throws Exception  {
...................
  }

   //Ersetzt, damit wir das Programm beenden können, wenn das Fenster geschlossen wird
  protected void processWindowEvent(WindowEvent e) {
.............
  }

  void btnGoStop_actionPerformed(ActionEvent e) {
    // Die Uhr wird gestartet/gestoppt
     // Die Beschriftung der Schaltfläche abrufen
    String libellé=btnGoStop.getText();
    // Starten?
    if(libellé.equals("Lancer")){
      // Erstellt den Thread, in dem die Uhr ausgeführt wird
      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }
      };//Thread definieren
       // Der Thread wird zum Starten freigegeben
      finHorloge=false;
       // Die Beschriftung der Schaltfläche wird geändert
      btnGoStop.setText("Arrêter");
      // Der Thread wird gestartet
      thHorloge.start();
      // Ende
      return;
    }//if
    // beenden
    if(libellé.equals("Arrêter")){
      // Der Thread wird angewiesen, zu stoppen
      finHorloge=true;
       // Die Beschriftung der Schaltfläche wird geändert
      btnGoStop.setText("Lancer");
       // Ende
      return;
    }//if
  }
} 

Wenn der Benutzer auf die Schaltfläche „Starten“ klickt, wird mithilfe einer anonymen Klasse ein Thread erstellt:

      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }

Die Methode run des Threads verweist auf die Methode runHorloge der Anwendung. Anschließend wird der Thread gestartet:

      // Der Thread wird gestartet
      thHorloge.start();

Die Methode runHorloge wird nun ausgeführt:

  private void runHorloge(){
    // Es wird so lange wiederholt, bis der Befehl zum Beenden erteilt wird
    while( ! finHorloge){
       // die Uhrzeit wird abgerufen
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // die Uhrzeit wird im Feld T angezeigt
      txtHorloge.setText(H);
       // eine Sekunde warten
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // Ausgabe mit Fehler
        System.exit(1);
      }//try
    }// while
  }// runHorloge

Das Prinzip der Methode ist wie folgt:

  1. Zeigt die aktuelle Uhrzeit im Textfeld txtHorloge an
  2. wartet 1 Sekunde
  3. wiederholt Schritt 1, nachdem zuvor der boolesche Wert finHorloge geprüft wurde, der auf „wahr“ gesetzt wird, sobald der Benutzer auf die Schaltfläche Arrêter klickt.

7.5. Uhr-Applet

Wir wandeln die vorherige grafische Anwendung auf die übliche Weise in ein Applet um und erstellen das folgende Dokument HTML appletHorloge.htm:

<html>
  <head>
    <title>Applet Horloge</title>
  </head>
  <body>
      <h2>Une applet horloge</h2>
    <applet
      code="appletHorloge.class"
      width="150"
      height="130"
    ></applet>
    </center>
  </body>
</html>

Wenn wir dieses Dokument durch Doppelklick direkt in IE laden, erhalten wir folgende Anzeige:

Image

Alle für das Applet erforderlichen Elemente befinden sich in diesem Beispiel im selben Ordner:

E:\data\serge\Jbuilder\horloge\1>dir
13/06/2002  12:17                3 174 appletHorloge.class
13/06/2002  12:17                  658 appletHorloge$1.class
13/06/2002  12:17                  512 appletHorloge$2.class
13/06/2002  12:20                  245 appletHorloge.htm

Unser Applet lässt sich verbessern. Wir haben gesagt, dass beim Laden des Applets die Methode init ausgeführt wird und anschließend die Methode start, sofern diese existiert. Außerdem wird beim Verlassen der Seite durch den Benutzer die Methode stop ausgeführt, sofern sie vorhanden ist. Kehrt der Benutzer zur Applet-Seite zurück, wird erneut die Methode start aufgerufen. Wenn ein Applet Threads für visuelle Animationen verwendet, werden häufig die Methoden start und stop des Applets verwendet, um die Threads zu starten und zu stoppen. Es ist nämlich sinnlos, dass ein Thread für visuelle Animationen im Hintergrund weiterläuft, während die Animation ausgeblendet ist.

Wir fügen unserem Applet daher die folgenden Methoden start und stop hinzu:

  public void stop(){
    // Die Seite ist ausgeblendet
     // Weiterverfolgung
    System.out.println("Page stop");
     // Die Seite ist ausgeblendet – der Thread wird beendet
    finHorloge=true;
  }

  public void start(){
     // Die Seite wird wieder angezeigt
     // Weiterverfolgung
    System.out.println("Page start");
     // Bei Bedarf wird ein neuer Uhr-Thread gestartet
    if(btnGoStop.getText().equals("Arrêter")){
      // Die Beschriftung wird geändert
      btnGoStop.setText("Lancer");
       // und es wird so getan, als hätte der Benutzer darauf geklickt
      btnGoStop_actionPerformed(null);
    }//if
  }//start

Außerdem haben wir in der Methode run des Threads eine Überwachung hinzugefügt, um festzustellen, wann er startet und stoppt:

  private void runHorloge(){
    // gefolgt von
    System.out.println("Thread horloge lancé");
    // wiederholen, bis wir den Befehl zum Stoppen erhalten
    while( ! finHorloge){
       // Wir rufen die Uhrzeit ab
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // wird im Feld T angezeigt
      txtHorloge.setText(H);
       // eine Sekunde warten
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // Beenden mit Fehler
        return;
      }//try
    }// while
    // Weiter
    System.out.println("Thread horloge terminé");
  }// runHorloge

Nun führen wir das Applet mit AppletViewer aus:

E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start    // Applet gestartet – Seite angezeigt
Thread horloge lancé    // Der Thread wird entsprechend gestartet
Page stop    // Applet in Symbolform
Thread horloge terminé    // Der Thread wird entsprechend beendet
Page start    // Applet erneut angezeigt
Thread horloge lancé    // Der Thread wird neu gestartet
Thread horloge terminé    // Drücken der Stopp-Taste
Thread horloge lancé    // Drücken der Start-Schaltfläche
Page stop    // Applet als Symbol    e
Thread horloge terminé    // Thread entsprechend gestoppt
Page start    // Applet wird neu angezeigt
Thread horloge lancé    // Thread entsprechend neu gestartet

Bei AppletViewer tritt das Ereignis start auf, wenn das Fenster von AppletViewer sichtbar ist, und das Ereignis stop, wenn es minimiert wird. Die obigen Ergebnisse zeigen, dass der Thread, wenn das Dokument HTML ausgeblendet wird, tatsächlich beendet wird, sofern er aktiv war.

7.6. Aufgabensynchronisation

In unserem vorherigen Beispiel gab es zwei Aufgaben:

  • die Hauptaufgabe, dargestellt durch die Anwendung selbst
  • die für die Uhr zuständige Aufgabe

Die Koordination zwischen den beiden Aufgaben erfolgte durch die Hauptaufgabe, die einen booleschen Wert setzte, um den Thread der Uhr anzuhalten. Wir befassen uns nun mit dem Problem des gleichzeitigen Zugriffs von Aufgaben auf gemeinsame Ressourcen, ein Problem, das auch als „Ressourcenfreigabe“ bekannt ist. Um dies zu veranschaulichen, betrachten wir zunächst ein Beispiel.

7.6.1. Eine nicht synchronisierte Zählung

Betrachten wir die folgende grafische Benutzeroberfläche:

Image

Nr.
Typ
Name
Rolle
1
JTextField
txtAGénérer
gibt die Anzahl der zu erzeugenden Threads an
2
JTextfield
(nicht editierbar)
txtGénéres
gibt die Anzahl der generierten Threads an
3
JTextField
(nicht bearbeitbar)
txtStatus
liefert Informationen zu aufgetretenen Fehlern und zur Anwendung selbst
4
JButton
btnGénérer
startet die Erzeugung der Threads

Die Anwendung funktioniert wie folgt:

  • Der Benutzer gibt die Anzahl der zu erzeugenden Threads in Feld 1 ein
  • Er startet die Erzeugung dieser Threads über die Schaltfläche 4
  • Die Threads lesen den Wert aus Feld 2, erhöhen ihn um eins und zeigen den neuen Wert an. Zu Beginn enthält dieses Feld den Wert 0.

Die erzeugten Threads teilen sich eine Ressource: den Wert von Feld 2. Wir möchten hier die Probleme aufzeigen, die in einer solchen Situation auftreten können. Hier ein Ausführungsbeispiel:

Image

Man sieht, dass die Erzeugung von 1000 Threads angefordert wurde, aber nur 7 gezählt wurden. Der relevante Code der Anwendung lautet wie folgt:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class interfaceSynchro extends JFrame {
  JPanel contentPane;
  JLabel jLabel1 = new JLabel();
  JTextField txtAGénérer = new JTextField();
  JButton btnGénérer = new JButton();
  JTextField txtStatus = new JTextField();
  JTextField txtGénérés = new JTextField();
  JLabel jLabel2 = new JLabel();

   // Instanzvariablen
    Thread[] tâches=null;   // die Threads
    int[] compteurs=null;   // Zähler

   //Rahmen erstellen
  public interfaceSynchro() {
..........
  }

   //Komponente initialisieren
  private void jbInit() throws Exception  {
......................
  }

   //Ersetzt, damit wir das Programm beenden können, wenn das Fenster geschlossen wird
  protected void processWindowEvent(WindowEvent e) {
..................
  }

  void btnGénérer_actionPerformed(ActionEvent e) {
     //Erzeugung der Threads

     // Die Anzahl der zu erzeugenden Threads wird gelesen
    int nbThreads=0;
    try{
      // Auslesen des Feldes, das die Anzahl der Threads enthält
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      // positiv >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //Fehler
      txtStatus.setText("Nombre invalide");
       // Neustart
      txtAGénérer.requestFocus();
      return;
    }//Abfangen

     // Zu Beginn wurden keine Threads generiert
    txtGénérés.setText("0");  // Aufgabenzähler auf 0

     // Threads werden generiert und gestartet
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // Thread i wird erstellt
      tâches[i]=new Thread() {
          public void run() {
          incrémente();
        }
      };//Thread i
       // Sein Name wird festgelegt
      tâches[i].setName(""+i);
      // seine Ausführung wird gestartet
      tâches[i].start();
    }//for
  }//generieren

   // inkrementieren
  private void incrémente(){
     // die Thread-Nummer abrufen
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // den Wert des Aufgabenzählers lesen
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // er wird erhöht
    compteurs[iThread]++;

     // 100 Millisekunden warten – der Thread verliert dann die CPU-Kontrolle
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }

     // Der neue Zählerstand wird angezeigt
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
    // Weiter
    System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
  }// inkrementieren

}// Klasse

Schauen wir uns den Code genauer an:

  • Das Fenster deklariert zwei Instanzvariablen:
   // Instanzvariablen
    Thread[] tâches=null;   // die Threads
    int[] compteurs=null;   // Zähler

Das Array tâches ist das Array der generierten Threads. Das Array compteurs wird dem Array tâches zugeordnet. Jede Aufgabe verfügt über einen eigenen Zähler, um den Wert des Feldes txtGénérés aus der grafischen Benutzeroberfläche abzurufen.

  • Bei einem Klick auf die Schaltfläche Générer wird die Methode btnGénérer_actionPerformed ausgeführt.
  • Diese beginnt damit, die Anzahl der zu erzeugenden Threads abzurufen. Falls erforderlich, wird ein Fehler gemeldet, wenn diese Anzahl nicht verwertbar ist. Anschließend erzeugt sie die angeforderten Threads, wobei sie deren Referenzen in einem Array vermerkt und jedem von ihnen eine Nummer zuweist. Die Methode run der generierten Threads ruft die Methode incrémente der Klasse auf. Alle Threads werden gestartet (start). Das Array der den Threads zugeordneten Zähler wird ebenfalls angelegt.
  • die Methode incrémente:
  • liest den aktuellen Wert des Feldes txtGénérés und speichert ihn im Zähler des aktuell ausgeführten Threads
  • wartet 100 ms, um den Prozessor absichtlich freizugeben
  • zeigt den neuen Wert im Feld txtGénérés an

Erläutern wir nun, warum die Zählung der Threads fehlerhaft ist. Nehmen wir an, es sollen 2 Threads generiert werden. Diese werden in unvorhersehbarer Reihenfolge ausgeführt. Einer von ihnen kommt als Erster an die Reihe und liest den Wert 0 aus dem Zähler. Er setzt ihn dann auf 1, schreibt diesen Wert jedoch nicht in das Fenster: Er unterbricht sich absichtlich für 100 ms. Dadurch verliert er die Prozessorausführung, die daraufhin an einen anderen Thread übergeben wird. Dieser verhält sich genauso wie der vorherige: Er liest den Zähler des Fensters aus und erhält die 0, die dort immer noch steht. Er setzt den Zähler auf 1 und unterbricht sich wie der vorherige für 100 ms. Der Prozessor wird daraufhin wieder dem ersten Thread zugewiesen: Dieser schreibt den Wert 1 in den Zähler des Fensters und beendet sich. Der Prozessor wird nun dem zweiten Thread zugewiesen, der ebenfalls 1 schreibt. Das Ergebnis ist falsch.

Wo liegt das Problem? Der zweite Thread hat einen falschen Wert gelesen, da der erste unterbrochen wurde, bevor er seine Aufgabe – die Aktualisierung des Zählers im Fenster – abgeschlossen hatte. Dies führt uns zum Begriff der kritischen Ressource und der kritischen Section eines Programms:

  • Eine kritische Ressource ist eine Ressource, die jeweils nur von einem Thread gleichzeitig gehalten werden kann. Hier ist die kritische Ressource der Zähler 2 im Fenster.
  • Ein kritischer Abschnitt eines Programms ist eine Folge von Anweisungen im Ausführungsfluss eines Threads, während der dieser auf eine kritische Ressource zugreift. Es muss sichergestellt werden, dass während dieses kritischen Abschnitts nur dieser Thread Zugriff auf die Ressource hat.

7.6.2. Eine durch eine Methode synchronisierte Zählung

Im vorherigen Beispiel führte jeder Thread die Methode incrémente des Fensters aus. Die Methode incrémente war wie folgt deklariert:

    private void incremente()

Nun deklarieren wir sie anders:

   // inkrementieren
  private synchronized void incrémente(){

Das Schlüsselwort „synchronized“ bedeutet, dass jeweils nur ein Thread die Methode incrémente ausführen kann. Betrachten wir die folgenden Notationen:

  • das Fensterobjekt F, das die Threads in btnGénérer_actionPerformed erstellt
  • die beiden von F erstellten Threads T1 und T2

Beide Threads werden von F erstellt und anschließend gestartet. Sie werden also beide die Methode F.run ausführen. Nehmen wir an, dass T1 als erstes ankommt. Es führt F.run und anschließend F.incremente aus, bei dem es sich um eine synchronisierte Methode handelt. Er liest den Wert 0 des Zählers, erhöht ihn um 1 und hält dann 100 ms lang an. Der Prozessor wird daraufhin an T2 übergeben, der wiederum F.run und anschließend F.incremente ausführt. Und hier kommt es zu einer Blockade, da der Thread T1 gerade F.incremente ausführt und das Schlüsselwort synchronized sicherstellt, dass jeweils nur ein Thread F.incremente ausführen kann. T2 verliert daraufhin seinerseits die Prozessorsteuerung, ohne den Wert des Zählers lesen zu können. Nach Ablauf der 100 ms erhält T1 die Prozessorsteuerung zurück, zeigt den Zählerstand 1 an, verlässt F.incremente und anschließend F.run und beendet sich. T2 erhält daraufhin den Prozessor zurück und kann diesmal F.incremente ausführen, da T1 diese Methode nicht mehr ausführt. T2 liest nun den Wert 1 des Zählers, erhöht ihn um 1 und hält 100 ms lang an. Nach 100 ms erhält es die Prozessorsteuerung zurück, zeigt den Wert 2 des Zählers an und beendet sich ebenfalls. Diesmal ist der ermittelte Wert korrekt. Hier ein getestetes Beispiel:

Image

7.6.3. Durch ein Objekt synchronisierte Zählung

Im vorherigen Beispiel wurde der Zugriff auf den Zähler txtGénérés durch eine Methode synchronisiert. Wenn das Fenster, das die Threads erstellt, F heißt, kann man auch sagen, dass die Methode F.incremente eine Ressource darstellt, die jeweils nur von einem einzigen Thread genutzt werden durfte. Es handelt sich also um eine kritische Ressource. Der synchronisierte Zugriff auf diese Ressource wurde durch das Schlüsselwort synchronized gewährleistet:

    private synchronized void incrémente()

Man könnte auch sagen, dass die kritische Ressource das Objekt F selbst ist. Dies ist strenger als in dem Fall, in dem die kritische Ressource F.incremente ist. Denn im letzteren Fall gilt: Wenn ein Thread T1 die Methode F.incremente ausführt, kann ein Thread T2 zwar nicht F.incremente ausführen, aber eine andere Methode des Objekts F, unabhängig davon, ob diese synchronisiert ist oder nicht. Falls das Objekt F selbst die kritische Ressource ist, werden, sobald ein Thread T1 einen synchronisierten Abschnitt dieses Objekts ausführt, alle anderen synchronisierten Abschnitte des Objekts für die anderen Threads unzugänglich. Wenn also ein Thread T1 die synchronisierte Methode F.incremente ausführt, kann ein Thread T2 nicht nur F.incremente, sondern auch jeden anderen synchronisierten Abschnitt von F nicht ausführen, und zwar selbst dann, wenn kein Thread diesen Abschnitt nutzt. Es handelt sich also um eine restriktivere Methode.

Nehmen wir also an, das Fenster werde zur kritischen Ressource. Dann schreiben wir:

   // inkrementieren
  private void incrémente(){
     // kritischer Abschnitt
    synchronized(this){
       // die Thread-Nummer wird abgerufen
      int iThread=0;
      try{
        iThread=Integer.parseInt(Thread.currentThread().getName());
      }catch(Exception ex){}
       // den Wert des Aufgabenzählers lesen
      try{
        compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
      } catch (Exception e){}
       // er wird erhöht
      compteurs[iThread]++;

       // 100 Millisekunden warten – der Thread verliert dann die Prozessorsteuerung
      try{
        Thread.sleep(100);
      } catch (Exception e){
        System.exit(0);
      }

       // Der neue Zählerstand wird angezeigt
      txtGénérés.setText("");
      txtGénérés.setText(""+compteurs[iThread]);
    }//synchronisiert
  }// inkrementiert

Alle Threads verwenden das Fenster this zur Synchronisation. Bei der Ausführung erhält man dieselben korrekten Ergebnisse wie zuvor. Tatsächlich kann man sich auf jedes beliebige Objekt synchronisieren, das allen Threads bekannt ist. Hier ist beispielsweise eine andere Methode, die zu denselben Ergebnissen führt:

   // Instanzvariablen
    Thread[] tâches=null;   // die Threads
    int[] compteurs=null;   // die Zähler
    Object synchro=new Object(); // ein Thread-Synchronisationsobjekt

   // inkrementiert
  private void incrémente(){
     // kritischer Abschnitt
    synchronized(synchro){
..............
    }//synchronisiert
  }// inkrementiert

Das Fenster erstellt ein Objekt vom Typ Object, das zur Synchronisation der Threads dient. Diese Methode ist besser als diejenige, die sich am Objekt this synchronisiert, da sie weniger einschränkend ist. Wenn sich hier ein Thread T1 im synchronisierten Abschnitt von incrémente befindet undein Thread T2 einen anderen synchronisierten Abschnitt desselben Objekts „this“ ausführen möchte, der jedoch durch ein anderes Objekt als synchro synchronisiert wird, ist dies möglich.

7.6.4. Ereignisgesteuerte Synchronisation

In diesem Fall verwenden wir eine boolesche Variable peutPasser, um einem Thread mitzuteilen, ob er einen kritischen Abschnitt betreten darf oder nicht. Ein Code ohne Synchronisation könnte wie folgt aussehen:

while(! peutPasser);        // Es wird darauf gewartet, dass peutPasser auf „wahr“ wechselt
peutPasser=false;            // kein anderer Thread darf hier durchlaufen
section critique;            // hier ist der Thread ganz allein
peutPasser=true;            // Ein anderer Thread darf den kritischen Abschnitt betreten

Die erste Anweisung, bei der ein Thread in einer Schleife wartet, bis peutPasser den Wert „wahr“ annimmt, ist ineffizient: Der Thread belegt den Prozessor unnötig. Man spricht hier von einer aktiven Wartezeit. Der Code lässt sich wie folgt verbessern:

while(! peutPasser){        // Es wird gewartet, bis peutPasser auf „wahr“ wechselt
   Thread.sleep(100);    // 100 ms lang angehalten wird
}
peutPasser=false;            // Es darf kein weiterer Thread durchlaufen
section critique;            // Hier ist der Thread ganz allein
peutPasser=true;            // Ein anderer Thread darf in den kritischen Abschnitt wechseln

Die Warteschleife ist hier besser: Wenn der Thread nicht weiterkommen kann, wird er für 100 ms in den Ruhezustand versetzt, bevor erneut geprüft wird, ob er weiterkommen kann oder nicht. Der Prozessor wird in der Zwischenzeit anderen Threads des Systems zugewiesen.

Beide Methoden sind eigentlich fehlerhaft: Sie verhindern nicht, dass zwei Threads gleichzeitig in den kritischen Abschnitt eindringen. Nehmen wir an, ein Thread T1 stellt fest, dass peutPasser wahr ist. Er fährt dann mit der nächsten Anweisung fort, in der er peutPasser auf „falsch“ setzt, um die anderen Threads zu blockieren. Allerdings kann er in diesem Moment sehr wohl unterbrochen werden, entweder weil seine Prozessorzeit abgelaufen ist, weil eine Aufgabe mit höherer Priorität den Prozessor angefordert hat oder aus einem anderen Grund. Das Ergebnis ist, dass er den Prozessor verliert. Er wird ihn etwas später wiedererlangen. In der Zwischenzeit erhalten andere Aufgaben den Prozessor, darunter möglicherweise ein Thread T2, der in einer Schleife wartet, bis peutPasser auf „wahr“ wechselt. Auch er wird feststellen, dass peutPasser auf „true“ steht (der erste Thread hatte keine Zeit, ihn auf „false“ zu setzen) und wird ebenfalls in den kritischen Abschnitt wechseln. Das hätte nicht passieren dürfen.

Die Sequenz


while(! peutPasser){            // Es wird darauf gewartet, dass peutPasser auf „wahr“ wechselt
   try{
        Thread.sleep(100);    // 100 ms Pause
    } catch (Exception e) {}
}// while
peutPasser=false;                // Es darf kein anderer Thread durchlaufen

ist eine kritische Sequenz, die durch eine Synchronisation geschützt werden muss. In Anlehnung an das vorherige Beispiel kann man schreiben:


    synchronized(synchro){
        while(! peutPasser){            // Es wird darauf gewartet, dass peutPasser den Wert „wahr“ annimmt
            try{
                Thread.sleep(100);    // 100 ms Pause
            } catch (Exception e) {}
        }//while
        peutPasser=false;                // kein anderer Thread darf passieren
    }// synchronisiert
    section critique;                    // Hier ist der Thread ganz allein
    peutPasser=true;                    // Ein anderer Thread darf den kritischen Abschnitt passieren

Dieses Beispiel funktioniert einwandfrei. Man kann es verbessern, indem man das halbaktive Warten des Threads vermeidet, während dieser regelmäßig den Wert des booleschen Ausdrucks peutPasser überwacht. Anstatt regelmäßig alle 100 ms aufzuwachen, um den Status von peutPasser zu überprüfen, kann er in den Ruhezustand wechseln und anweisen, dass er geweckt wird, sobald peutPasser den Wert „wahr“ annimmt. Dies lässt sich wie folgt schreiben:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // Wenn man nicht weiterkommen kann, wartet man
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // kein anderer Thread darf passieren
}// synchronisiert

Die Operation synchro.wait() kann nur von einem Thread ausgeführt werden, der vorübergehend „Eigentümer“ des Objekts synchro ist. Hier lautet die Sequenz:


synchronized(synchro){

}// synchronized

die sicherstellt, dass der Thread Eigentümer des Objekts synchro ist. Durch die Operation synchro.wait() gibt der Thread das Eigentumsrecht an der Synchronisationssperre ab. Warum ist das so? In der Regel, weil ihm Ressourcen fehlen, um die Arbeit fortzusetzen. Anstatt also die anderen Threads zu blockieren, die auf die Ressource synchro warten, gibt er diese frei und wartet selbst auf die Ressource, die ihm fehlt. In unserem Beispiel wartet er darauf, dass der boolesche Wert peutPasser auf „wahr“ wechselt. Wie wird er über dieses Ereignis benachrichtigt? Auf folgende Weise:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // Wenn der Übergang nicht möglich ist, wird gewartet
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // Kein anderer Thread darf passieren
}// synchronized
section critique...
synchronized(synchro){
      synchro.notify();
    }

Betrachten wir den ersten Thread, der die Synchronisationssperre passiert. Nennen wir ihn T1. Nehmen wir an, er findet den booleschen Wert peutPasser als „wahr“, da er der Erste ist. Er setzt ihn daher auf „falsch“. Anschließend verlässt er den kritischen Abschnitt, der durch das Objekt synchro gesperrt ist. Ein anderer Thread kann nun in den kritischen Abschnitt eintreten, um den Wert von peutPasser zu prüfen. Er wird feststellen, dass dieser falsch ist, und sich daraufhin in den Wartezustand für ein Ereignis (wait) versetzen. Dabei gibt er den Besitz des Objekts synchro ab. Ein weiterer Thread kann nun in den kritischen Abschnitt eintreten: Auch er wird warten, da peutPasser als falsch erkannt wird. Es können also mehrere Threads auf ein Ereignis am Objekt synchro warten.

Kehren wir zum Thread T1 zurück, an den die Kontrolle übergeben wurde. Er führt den kritischen Abschnitt aus und signalisiert anschließend, dass nun ein anderer Thread an die Reihe kommen kann. Dies geschieht mit folgender Sequenz:


synchronized(synchro){
      synchro.notify();
    }

Zunächst muss er das Objekt synchro mit dem Befehl synchronized wieder in Besitz nehmen. Das sollte kein Problem darstellen, da er mit anderen Threads konkurriert, die, falls sie vorübergehend das Objekt synchro erhalten, dieses durch einen Befehl wait freigeben müssen, weil sie feststellen, dass peutPasser falsch ist. Somit wird unser Thread T1 schließlich die Eigentumsrechte am Objekt synchro erhalten. Ist dies geschehen, signalisiert er durch die Operation synchro.notify, dass einer der durch einen synchro.wait blockierten Threads geweckt werden muss. Anschließend gibt er erneut die Eigentumsrechte am Objekt synchro ab, die dann an einen der wartenden Threads übertragen werden. Dieser setzt seine Ausführung mit der Anweisung fort, die auf den Befehl wait folgt, der ihn in den Wartezustand versetzt hatte. Im Gegenzug führt er den kritischen Abschnitt aus und führt einen Befehl synchro.notify aus, um einen weiteren Thread freizugeben. Und so weiter.

Betrachten wir diese Funktionsweise am Beispiel der bereits untersuchten Zählung.

  void btnGénérer_actionPerformed(ActionEvent e) {
     //Erzeugung der Threads

     // Die Anzahl der zu erzeugenden Threads wird gelesen
    int nbThreads=0;
    try{
      // Auslesen des Feldes, das die Anzahl der Threads enthält
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      // positiv >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //Fehler
      txtStatus.setText("Nombre invalide");
       // Neustart
      txtAGénérer.requestFocus();
      return;
    }//Abfangen

     // RAZ Aufgabenzähler
    txtGénérés.setText("0");  // Aufgabenzähler auf 0
    // Der erste Thread kann weiterlaufen
    peutPasser=true;
     // Threads werden generiert und gestartet
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // Thread i wird erstellt
      tâches[i]=new Thread() {
          public void run() {
          synchronise();
        }
      };//Thread i
       // Sein Name wird definiert
      tâches[i].setName(""+i);
      // Man startet seine Ausführung
      tâches[i].start();
    }//for
  }//generieren

Nun führen die Threads nicht mehr die Methode incrémente aus, sondern die folgende Methode synchronise:

   // Synchronisationsschritt der Threads
  public void synchronise(){
     // Zugriff auf den kritischen Abschnitt anfordern
    synchronized(synchro){
      try{
        // Darf man weiter?
        if(! peutPasser){
          System.out.println(Thread.currentThread().getName()+ " en attente");
          synchro.wait();
        }
         // Wir sind durch – anderen Threads wird der Durchgang verwehrt
        peutPasser=false;
      } catch(Exception e){
        txtStatus.setText(""+e);
                    return;
      }//try
    }// synchronized

    // kritischer Abschnitt
    System.out.println(Thread.currentThread().getName()+ " passé");
    incrémente();

     // Wir sind fertig – wir geben einen eventuell am Eingang des kritischen Abschnitts blockierten Thread frei
    peutPasser=true;
    System.out.println(Thread.currentThread().getName()+ " terminé");
    synchronized(synchro){
      synchro.notify();
    }// synchronized
  } // synchronisiert

Die Methode synchronise dient dazu, die Threads nacheinander abzuarbeiten. Dazu verwendet sie eine Synchronisationsvariable synchro. Die Methode incrémente ist nun nicht mehr durch das Schlüsselwort synchronized geschützt:

   // inkrementiert
  private void incrémente(){
     // Die Thread-Nummer wird abgerufen
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // der Wert des Aufgabenzählers wird gelesen
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // er wird inkrementiert
    compteurs[iThread]++;
     // 100 Millisekunden warten – der Thread verliert dann die CPU-Kontrolle
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }
     // Der neue Zählerstand wird angezeigt
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
  }// inkrementieren

Bei 5 Threads ergaben sich folgende Ergebnisse:

0 passé
1 en attente
2 en attente
3 en attente
4 en attente
0 terminé
1 passé
1 terminé
2 passé
2 terminé
3 passé
3 terminé
4 passé
4 terminé

Seien T0 bis T4 die 5 von der Anwendung erzeugten Threads. T0 erlangt als erstes die Eigentumsrechte an der Sperre synchro und stellt fest, dass peutPasser wahr ist. Er setzt peutPasser auf „falsch“ und fährt fort: Das ist der Sinn der ersten Nachricht passé. Aller Wahrscheinlichkeit nach fährt er fort und führt den kritischen Abschnitt aus, insbesondere die Methode incrémente. In dieser wird er für 100 ms in den Ruhezustand versetzt (sleep). Er gibt also den Prozessor frei. Dieser wird einem anderen Thread zugewiesen, dem Thread T1, der daraufhin die Eigentumsrechte am Objekt synchro erhält. Er stellt fest, dass er nicht weiterkommen kann, und geht in den Wartezustand über (wait). Daraufhin gibt er die Eigentumsrechte am Objekt synchro sowie den Prozessor frei. Dieser wird dem Thread T2 zugewiesen, dem dasselbe Schicksal widerfährt. Während der 100 ms, in denen T0 angehalten ist, werden die Threads T1 bis T4 daher in den Wartezustand versetzt. Das ist die Bedeutung der vier Meldungen „in Wartestellung“. Nach 100 ms erhält T0 den Prozessor zurück und beendet seine Arbeit: Das ist die Bedeutung der Meldung „0 terminé“. Anschließend gibt er einen der blockierten Threads frei und beendet sich selbst. Der freigegebene Prozessor wird dann einem verfügbaren Thread zugewiesen: demjenigen, der gerade freigegeben wurde. Hier ist das T1. Der Thread T1 tritt nun in den kritischen Abschnitt ein: Das ist die Bedeutung der Meldung „1 passé“. Er erledigt seine Aufgabe und wird seinerseits für 100 ms angehalten. Der Prozessor steht nun für einen anderen Thread zur Verfügung, aber alle warten auf ein Ereignis: Keiner von ihnen kann den Prozessor übernehmen. Nach 100 ms erhält der Thread T1 den Prozessor zurück und wird beendet: Das ist die Bedeutung der Meldung „1 terminé“. Die Threads T1 bis T4 werden sich genauso verhalten wie T1: Das ist die Bedeutung der drei Meldungsreihen: „abgelaufen“, „beendet“.