Skip to content

7. Threads

7.1. Einleitung

Wenn eine Anwendung gestartet wird, läuft sie in einem Ausführungsablauf, der als Thread bezeichnet wird. Die Klasse, die einen Thread modelliert, ist die Klasse *java.lang.Thread*, die über die folgenden Eigenschaften und Methoden verfügt:

currentThread()
gibt den aktuell ausgeführten Thread zurück
setName()
legt den Namen des Threads fest
getName()
Thread-Name
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 vorangehende start-Methode ausgeführt wurde
sleep(n)
unterbricht die Ausführung eines Threads für n Millisekunden
join()
blockierende Operation – wartet, bis der Thread beendet ist, bevor mit der nächsten Anweisung fortgefahren wird

Die am häufigsten verwendeten Konstruktoren sind die folgenden:

Thread()
Erstellt eine Referenz auf eine asynchrone Aufgabe. Diese Aufgabe ist noch inaktiv. Die erstellte Aufgabe muss über eine run-Methode verfügen: Meistens wird eine von Thread abgeleitete Klasse verwendet.
Thread(Runnable-Objekt)
Wie oben, jedoch implementiert das als Parameter übergebene Runnable-Objekt die run-Methode.

Sehen wir uns eine einfache Anwendung an, die die Existenz eines Hauptausführungsthreads demonstriert – jenem, in dem die main-Funktion einer Klasse ausgeführt wird:

// use of threads

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

public class thread1{
    public static void main(String[] arg)throws Exception {
         // init current thread
        Thread main=Thread.currentThread();
         // display
        System.out.println("Thread courant : " + main.getName());
         // we change the name
        main.setName("myMainThread");
         // check
        System.out.println("Thread courant : " + main.getName());

         // infinite loop
        while(true){
        // time recovery
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
             // display
            System.out.println(main.getName() + " : " +H);
             // temporary shutdown
            Thread.sleep(1000);
        }//while
    }//hand
}//class

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 vorangegangene Beispiel veranschaulicht die folgenden Punkte:

  • Die Hauptfunktion läuft korrekt in einem Thread
  • wir können über Thread.currentThread() auf die Eigenschaften dieses Threads zugreifen
  • die Rolle der sleep-Methode. Hier ruht der Thread, der main ausführt, zwischen jeder Anzeige 1 Sekunde lang.

7.2. Erstellen von Ausführungsthreads

Es gibt Anwendungen, in denen Codeabschnitte „gleichzeitig“ in verschiedenen Ausführungsthreads ausgeführt werden. Wenn wir sagen, dass Threads gleichzeitig laufen, verwenden wir diesen Begriff oft im weiteren Sinne. Verfügt der Rechner nur über einen Prozessor, was immer noch häufig der Fall ist, teilen sich die Threads diesen Prozessor: Jeder hat 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 seiner Priorität, die einen Standardwert hat, aber auch programmgesteuert festgelegt werden kann. Wenn ein Thread den Prozessor hat, nutzt er diesen normalerweise für die gesamte ihm zugewiesene Zeit. Er kann ihn jedoch vorzeitig freigeben:

  • indem er auf ein Ereignis wartet (wait, join)
  • indem er für einen bestimmten Zeitraum in den Ruhezustand wechselt (sleep)
  • Ein Thread T kann auf verschiedene Arten erstellt werden
    • durch Erweitern der Thread-Klasse und Überschreiben ihrer run-Methode
    • durch Implementierung der Runnable-Schnittstelle 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 vorgenannten Konstruktors ist daher jede Klasseninstanz, die diese run-Methode implementiert.

Im folgenden Beispiel werden Threads mithilfe einer anonymen Klasse erstellt, die die Klasse „Thread“ erweitert:

            // create thread i
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//def tasks[i]

Die Methode „run“ ruft hier lediglich die Methode „display“ auf.

  • Die Ausführung des Threads T wird durch T.start() gestartet: Diese Methode gehört zur Thread-Klasse und führt eine Reihe von Initialisierungen durch, bevor sie automatisch die run-Methode des Threads oder der Runnable-Schnittstelle aufruft. Das Programm, das die Anweisung T.start() ausführt, wartet nicht auf den Abschluss der Aufgabe T: Es fährt sofort mit der nächsten Anweisung fort. Wir haben nun zwei parallel laufende Aufgaben. Diese müssen oft miteinander kommunizieren können, um den Status der gemeinsam zu erledigenden Arbeit zu kennen. Dies ist das Problem der Thread-Synchronisation.
  • Einmal gestartet, läuft der Thread autonom. Er stoppt, sobald die von ihm ausgeführte run-Funktion ihre Arbeit beendet hat.
  • Mit T.join() können wir darauf warten, dass der Thread T die Ausführung beendet. Dies ist eine blockierende Anweisung: Das Programm, das sie ausführt, wird blockiert, bis die Aufgabe T ihre Arbeit beendet hat. Es ist auch ein Mittel zur Synchronisation.

Betrachten wir das folgende Programm:

// use of threads

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

public class thread2{
    public static void main(String[] arg) {
         // init current thread
        Thread main=Thread.currentThread();
         // give a name to the current thread
        main.setName("myMainThread");
         // start of hand
        System.out.println("début du thread " +main.getName());

         // creation of execution threads
        Thread[] tâches=new Thread[5];
        for(int i=0;i<tâches.length;i++){
             // create thread i
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//def tasks[i]
             // set the thread name
            tâches[i].setName(""+i);
             // start execution of thread i
            tâches[i].start();
        }//for

         // end of hand
        System.out.println("fin du thread " +main.getName());
    }//Main

    public static void affiche() {
         // time recovery
    Calendar calendrier=Calendar.getInstance();
    String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // display start of execution
        System.out.println("Début d'exécution de la méthode affiche dans le Thread " + 
        Thread.currentThread().getName()+ " : " + H);
         // sleep for 1 s
        try{
        Thread.sleep(1000);
    }catch (Exception ex){}
     // time recovery
    calendrier=Calendar.getInstance();    
    H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // display end of run
        System.out.println("Fin d'exécution de la méthode affiche dans le Thread " 
    +Thread.currentThread().getName()+ " : " + H);
    }// poster
}//class

Der Haupt-Thread, der die main-Funktion ausführt, erstellt 5 weitere Threads, die für die Ausführung der statischen display-Methode zuständig sind. 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:

  • Erstens sehen wir, dass der Start der Ausführung eines Threads nicht blockierend ist. Die Hauptmethode startete die Ausführung von 5 Threads parallel und beendete die Ausführung vor ihnen. Der Vorgang
            // on lance l'exécution du thread i
            tâches[i].start();

startet die Ausführung des Threads tasks[i], doch sobald dies geschehen ist, wird die Ausführung sofort mit der nächsten Anweisung fortgesetzt, ohne auf den Abschluss des Threads zu warten.

  • Alle erstellten Threads müssen die Methode display ausführen. Die Reihenfolge der Ausführung ist unvorhersehbar. Auch wenn es im Beispiel so aussieht, als würde die Reihenfolge der Ausführung der Reihenfolge entsprechen, in der die Threads gestartet wurden, lassen sich daraus keine allgemeinen Schlussfolgerungen ziehen. Das Betriebssystem verfügt hier über 6 Threads und einen Prozessor. Es wird den Prozessor diesen 6 Threads nach seinen eigenen Regeln zuweisen.
  • Die Ergebnisse zeigen eine Auswirkung der sleep-Methode. Im Beispiel führt Thread 0 als erster die display-Methode aus. Die Meldung zum Start der Ausführung wird angezeigt, dann führt er die sleep-Methode aus, die ihn für 1 Sekunde unterbricht. Er verliert daraufhin den Prozessor, der für einen anderen Thread verfügbar wird. Das Beispiel zeigt, dass Thread 1 ihn erhält. Thread 1 folgt dem gleichen Weg wie die anderen Threads. Wenn die 1-sekündige Sleep-Pause für Thread 0 endet, kann dessen Ausführung fortgesetzt werden. Das System weist ihm den Prozessor zu, und er kann die Ausführung der display-Methode abschließen.

Ändern wir unser Programm so, dass die *main*-Methode mit den folgenden Anweisungen endet:

         // end of hand
        System.out.println("fin du thread " +main.getName());
     // application shutdown
    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 Hauptmethode die Anweisung ausführt:

    System.exit(0);

werden alle Threads der Anwendung angehalten, nicht nur der Haupt-Thread. Die Hauptmethode möchte möglicherweise warten, bis die von ihr erstellten Threads ihre Ausführung beendet haben, bevor sie sich selbst beendet. Dies kann mithilfe der join-Methode der Thread-Klasse erfolgen:

     // waiting for all threads
        for(int i=0;i<tâches.length;i++){
            // we wait for thread i
            tâches[i].join();
    }//for  

         // end of hand
        System.out.println("fin du thread " +main.getName());
     // application shutdown
    System.exit(0);

Dies führt zu folgenden Ergebnissen:

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. Die Vorteile von Threads

Nachdem wir nun die Existenz eines Standard-Threads – derjenige, der die Main-Methode ausführt – hervorgehoben haben und wissen, wie man weitere erstellt, wollen wir uns die Vorteile von Threads für uns ansehen und erklären, warum wir sie hier vorstellen. Es gibt eine Art von Anwendung, die sich besonders gut für die Verwendung von Threads eignet: Client-Server-Anwendungen im Internet. In einer solchen Anwendung reagiert ein Server auf Rechner S1 auf Anfragen von Clients auf entfernten Rechnern C1, C2, ..., Cn.

Image

Wir nutzen täglich Internetanwendungen, die diesem Muster folgen: Webdienste, E-Mail, das Durchsuchen von Foren, Dateiübertragungen... In der obigen Abbildung muss der Server S1 die Clients C1, C2, ..., Cn gleichzeitig bedienen. Nehmen wir das Beispiel eines FTP-Servers (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 monopolisiert. Üblicherweise erstellt der Server so viele Ausführungsthreads, wie es Clients gibt. Jeder Thread ist dann für die Betreuung eines bestimmten Clients zuständig. Da der Prozessor zyklisch unter allen aktiven Threads auf dem Rechner aufgeteilt wird, verbringt der Server mit jedem Client nur wenig Zeit und gewährleistet so die Parallelität des Dienstes.

Image

7.4. Eine grafische Uhr-App

Betrachten Sie 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 die Zeit jede Sekunde aktualisieren. Gleichzeitig müssen Ereignisse im Fenster überwacht werden: Wenn der Benutzer auf die Schaltfläche „Stopp“ klickt, muss die Uhr angehalten werden. Hier haben wir zwei parallele und asynchrone Aufgaben: Der Benutzer kann jederzeit klicken.

Betrachten wir den Moment, in dem die Uhr noch nicht gestartet wurde und der Benutzer auf die Schaltfläche „Start“ klickt. Dies ist ein klassisches Ereignis, und man könnte meinen, dass eine Methode des Threads, in dem das Fenster läuft, die Uhr dann verwalten könnte. Wenn jedoch eine Methode der GUI-Anwendung läuft, hört ihr Thread nicht mehr auf GUI-Ereignisse. Diese Ereignisse treten auf und werden in eine Warteschlange gestellt, um von der Anwendung verarbeitet zu werden, sobald die aktuell laufende Methode beendet ist. In unserem Uhrenbeispiel läuft die Methode immer weiter, da sie nur durch Klicken auf die Schaltfläche „Stopp“ angehalten werden kann. Dieses Ereignis wird jedoch erst verarbeitet, wenn die Methode beendet ist. Wir befinden uns in einer Endlosschleife.

Die Lösung für dieses Problem wäre, dass beim Klicken des Benutzers auf die Schaltfläche „Start“ eine Aufgabe zur Verwaltung der Uhr gestartet wird, die Anwendung aber weiterhin auf Ereignisse im Fenster warten kann. Wir hätten dann zwei separate Aufgaben, die parallel laufen:

  • Uhrenverwaltung
  • Abhören von Fensterereignissen

Kehren wir zu unserer grafischen Uhr zurück:

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

Der Quellcode für die mit JBuilder erstellte 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();

  // instance attributes
  boolean finHorloge=true;

   //Building the frame
  public interfaceHorloge() {
    enableEvents(AWTEvent.WINDOW_EVENT_MASK);
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

  private void runHorloge(){
    // we don't stop until we're told to stop
    while( ! finHorloge){
       // time recovery
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // it is displayed in the T
      txtHorloge.setText(H);
       // waiting for a second
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // output with error
        System.exit(1);
      }//try
    }// while
  }// runHorloge

   //Initialize component
  private void jbInit() throws Exception  {
...................
  }

   //Replaced, so we can get out when the window is closed
  protected void processWindowEvent(WindowEvent e) {
.............
  }

  void btnGoStop_actionPerformed(ActionEvent e) {
    // start/stop clock
     // retrieve the button label
    String libellé=btnGoStop.getText();
    // launch?
    if(libellé.equals("Lancer")){
      // create the thread in which the clock will run
      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }
      };//def thread
       // authorize the thread to start
      finHorloge=false;
       // change the button label
      btnGoStop.setText("Arrêter");
      // start the thread
      thHorloge.start();
      // end
      return;
    }//if
    // stop
    if(libellé.equals("Arrêter")){
      // the thread is told to stop
      finHorloge=true;
       // change the button label
      btnGoStop.setText("Lancer");
       // end
      return;
    }//if
  }
} 

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

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

Die run-Methode des Threads ruft die runHorloge-Methode der Anwendung auf. Sobald dies geschehen ist, wird der Thread gestartet:

      // on lance le thread
      thHorloge.start();

Die Methode „runHorloge“ wird dann ausgeführt:

  private void runHorloge(){
    // we don't stop until we're told to stop
    while( ! finHorloge){
       // time recovery
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // it is displayed in the T
      txtHorloge.setText(H);
       // waiting for a second
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // output with error
        System.exit(1);
      }//try
    }// while
  }// runHorloge

Das Prinzip der Methode ist wie folgt:

  1. Zeigt die aktuelle Uhrzeit im Textfeld „txtHorloge“ an
  2. hält 1 Sekunde lang an
  3. setzt Schritt 1 fort, nachdem zuvor die boolesche Variable finHorloge überprüft wurde, die auf „true“ gesetzt wird, wenn der Benutzer auf die Schaltfläche „Stopp“ klickt.

7.5. Ein „ “-Uhr-Applet

Wir wandeln die vorherige grafische Anwendung mithilfe der Standardmethode in ein Applet um und erstellen das folgende HTML-Dokument, 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 Doppelklicken direkt im IE laden, erhalten wir folgende Anzeige:

Image

In diesem Beispiel befinden sich alle vom Applet benötigten Elemente 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 erwähnt, dass beim Laden des Applets die init-Methode ausgeführt wird, gefolgt von der start-Methode, sofern vorhanden. Außerdem wird beim Verlassen der Seite durch den Benutzer die stop-Methode ausgeführt, sofern vorhanden. Kehrt der Benutzer zur Seite des Applets zurück, wird die start-Methode erneut aufgerufen. Wenn ein Applet visuelle Animations-Threads implementiert, werden die Start- und Stop-Methoden des Applets häufig zum Starten und Beenden der Threads verwendet. Es ist in der Tat unnötig, dass ein visueller Animations-Thread im Hintergrund weiterläuft, während die Animation ausgeblendet ist.

Also fügen wir unserem Applet die folgenden Start- und Stopp-Methoden hinzu:

  public void stop(){
    // the page is hidden
     // follow-up
    System.out.println("Page stop");
     // the page is hidden - stop the thread
    finHorloge=true;
  }

  public void start(){
     // the page reappears
     // follow-up
    System.out.println("Page start");
     // restart a new clock thread if necessary
    if(btnGoStop.getText().equals("Arrêter")){
      // change the wording
      btnGoStop.setText("Lancer");
       // and act as if the user had clicked on it
      btnGoStop_actionPerformed(null);
    }//if
  }//start

Zusätzlich haben wir in der run-Methode des Threads eine Überprüfung hinzugefügt, um festzustellen, wann er startet und stoppt:

  private void runHorloge(){
    // follow-up
    System.out.println("Thread horloge lancé");
    // we don't stop until we're told to stop
    while( ! finHorloge){
       // time recovery
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // it is displayed in the T
      txtHorloge.setText(H);
       // waiting for a second
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // output with error
        return;
      }//try
    }// while
    // follow-up
    System.out.println("Thread horloge terminé");
  }// runHorloge

Nun führen wir das Applet mit dem AppletViewer aus:

E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start    // applet launched - page displayed
Thread horloge lancé    // the thread is launched accordingly
Page stop    // iconized applet
Thread horloge terminé    // the thread is stopped accordingly
Page start    // applet redisplayed
Thread horloge lancé    // the thread is restarted
Thread horloge terminé    // press stop button
Thread horloge lancé    // press start button
Page stop    // icon applet
Thread horloge terminé    // thread arrested accordingly
Page start    // applet redisplay
Thread horloge lancé    // thread relaunched accordingly

Bei AppletViewer tritt das Start-Ereignis ein, wenn das AppletViewer-Fenster sichtbar ist, und das Stopp-Ereignis, wenn es minimiert wird. Die obigen Ergebnisse zeigen, dass der Thread tatsächlich gestoppt wird, wenn das HTML-Dokument ausgeblendet wird, sofern er zuvor aktiv war.

7.6. Aufgabensynchronisation

In unserem vorherigen Beispiel gab es zwei Aufgaben:

  • die Hauptaufgabe, die durch die Anwendung selbst repräsentiert wird
  • die für die Uhr zuständige Aufgabe

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

7.6.1. Ein nicht synchronisierter Zähler

Betrachten Sie die folgende grafische Benutzeroberfläche:

Image

Nr.
Typ
Name
Rolle
1
JTextField
txtAGenerate
gibt die Anzahl der zu generierenden Threads an
2
JTextField
(nicht editierbar)
txtGenerated
zeigt die Anzahl der generierten Threads an
3
JTextField
(nicht editierbar)
txtStatus
liefert Informationen zu aufgetretenen Fehlern und zur Anwendung selbst
4
JButton
btnGenerate
startet die Thread-Generierung

Die Anwendung funktioniert wie folgt:

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

Die generierten Threads teilen sich eine Ressource: den Wert von Feld 2. Hier möchten wir die Probleme aufzeigen, die in einer solchen Situation auftreten. Hier ist ein Beispiel für die Ausführung:

Image

Wir sehen, dass wir die Erzeugung von 1.000 Threads angefordert haben, aber nur 7 gezählt wurden. Der relevante Code für die 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();

   // instance variables
    Thread[] tâches=null;   // threads
    int[] compteurs=null;   // meters

   //Building the frame
  public interfaceSynchro() {
..........
  }

   //Initialize component
  private void jbInit() throws Exception  {
......................
  }

   //Replaced, so we can get out when the window is closed
  protected void processWindowEvent(WindowEvent e) {
..................
  }

  void btnGénérer_actionPerformed(ActionEvent e) {
     //thread generation

     // read the number of threads to generate
    int nbThreads=0;
    try{
      // read the field containing the number of threads
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      // positive >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //error
      txtStatus.setText("Nombre invalide");
       // we start again
      txtAGénérer.requestFocus();
      return;
    }//catch

     // initially no threads generated
    txtGénérés.setText("0");  // task cpteur at 0

     // threads are generated and launched
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // create thread i
      tâches[i]=new Thread() {
          public void run() {
          incrémente();
        }
      };//thread i
       // define its name
      tâches[i].setName(""+i);
      // start execution
      tâches[i].start();
    }//for
  }//generate

   // increment
  private void incrémente(){
     // retrieve the thread number
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // read the job counter value
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // increment it
    compteurs[iThread]++;

     // wait 100 milliseconds - the thread will then lose the processor
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }

     // the new counter is displayed
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
    // follow-up
    System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
  }// increment

}// class

Schauen wir uns den Code einmal genauer an:

  • Das Fenster deklariert zwei Instanzvariablen:
   // instance variables
    Thread[] tâches=null;   // threads
    int[] compteurs=null;   // meters

Das Array „tasks“ ist das Array der generierten Threads. Das Array „counters“ ist mit dem Array „tasks“ verknüpft. Jede Aufgabe verfügt über einen eigenen Zähler, um den Wert des Feldes „txtGenerated“ aus der GUI abzurufen.

  • Wenn die Schaltfläche „Generate“ angeklickt wird, wird die Methode btnGenerate_actionPerformed ausgeführt.
  • Diese Methode beginnt damit, die Anzahl der zu generierenden Threads abzurufen. Falls erforderlich, wird ein Fehler gemeldet, wenn diese Zahl ungültig ist. Anschließend generiert sie die angeforderten Threads, wobei darauf geachtet wird, ihre Referenzen in einem Array zu speichern und jedem eine Nummer zuzuweisen. Die Methode „run“ der generierten Threads ruft die Methode „increment“ der Klasse auf. Alle Threads werden gestartet (start). Das mit den Threads verknüpfte Array „counters“ wird ebenfalls erstellt.
  • Die Methode „increment“:
  • liest den aktuellen Wert des Feldes „txtGénérés“ und speichert ihn im Zähler des aktuell laufenden Threads
  • hält für 100 ms an, um den Prozessor absichtlich im Leerlauf zu lassen
  • zeigt den neuen Wert im Feld „txtGenerated“ an

Erklären wir nun, warum die Thread-Anzahl falsch ist. Angenommen, es gibt 2 Threads zu generieren. Sie werden in einer unvorhersehbaren Reihenfolge ausgeführt. Einer von ihnen läuft zuerst und liest den Wert 0 aus dem Zähler. Er setzt ihn dann auf 1, schreibt aber kein „ “ in das Fenster: Er pausiert absichtlich für 100 ms. Dann verliert er den Prozessor, der daraufhin an einen anderen Thread übergeben wird. Dieser Thread verhält sich genauso wie der vorherige: Er liest den Zähler des Fensters und findet dort noch die 0 vor. Er setzt den Zähler auf 1 und hält, wie der vorherige, 100 ms lang an. Der Prozessor wird dann wieder dem ersten Thread zugewiesen: Dieser Thread 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. Am Ende erhalten wir ein falsches Ergebnis.

Woher kommt das Problem? Der zweite Thread hat einen falschen Wert gelesen, weil der erste Thread unterbrochen wurde, bevor er seine Aufgabe – die Aktualisierung des Zählers im Fenster – beendet hatte. Dies führt uns zum Konzept einer kritischen Ressource und eines kritischen Abschnitts in einem Programm:

  • Eine kritische Ressource ist eine Ressource, die jeweils nur von einem Thread 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ührungsablauf eines Threads, während der dieser auf eine kritische Ressource zugreift. Wir müssen sicherstellen, dass während dieses kritischen Abschnitts nur dieser Thread Zugriff auf die Ressource hat.

7.6.2. Synchronisiertes Zählen nach Methode

Im vorherigen Beispiel führte jeder Thread die Increment-Methode des Fensters aus. Die Increment-Methode wurde wie folgt deklariert:

    private void incremente()

Nun deklarieren wir sie anders:

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

Das Schlüsselwort **synchronized** bedeutet, dass jeweils nur ein Thread die Methode increment ausführen kann. Betrachten Sie die folgende Notation:

  • das Fensterobjekt F, das in btnGenerate_actionPerformed Threads erstellt
  • zwei von F erstellte Threads T1 und T2

Beide Threads werden von F erstellt und anschließend gestartet. Sie führen daher beide die Methode F.run aus. Angenommen, T1 kommt zuerst an. Er führt F.run und anschließend F.increment aus, eine synchronisierte Methode. Er liest den Zählerwert 0, erhöht ihn um 1 und pausiert dann für 100 ms. Der Prozessor wird dann an T2 übergeben, der wiederum F.run und anschließend F.increment ausführt. An diesem Punkt wird T2 blockiert, da Thread T1 gerade F.increment ausführt und das Schlüsselwort synchronized sicherstellt, dass jeweils nur ein Thread F.increment ausführen kann. T2 verliert daraufhin den Prozessor, ohne den Wert des Zählers gelesen zu haben. Nach 100 ms erhält T1 den Prozessor zurück, zeigt den Zählerstand 1 an, verlässt F.increment und anschließend F.run und beendet die Ausführung. T2 erhält dann den Prozessor zurück und kann nun F.incremente ausführen, da T1 diese Methode nicht mehr ausführt. T2 liest dann den Zählerwert 1, inkrementiert ihn und pausiert für 100 ms. Nach 100 ms erhält er den Prozessor zurück, zeigt den Zählerwert 2 an und beendet sich ebenfalls. Diesmal ist der erhaltene Wert korrekt. Hier ist ein getestetes Beispiel:

Image

7.6.3. Zählen, synchronisiert durch ein Objekt

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, können wir auch sagen, dass die Methode F.incremente eine Ressource darstellt, die jeweils nur von einem einzigen Thread verwendet werden sollte. Es handelt sich also um eine kritische Ressource. Der synchronisierte Zugriff auf diese Ressource wurde durch das Schlüsselwort synchronized sichergestellt:

    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.increment ist. Denn im letzteren Fall kann ein Thread T2, wenn ein Thread T1 F.increment ausführt, zwar F.increment nicht ausführen, aber eine andere Methode des Objekts F, unabhängig davon, ob diese synchronisiert ist oder nicht. In dem Fall, in dem das Objekt F selbst die kritische Ressource ist, werden, wenn ein Thread T1 einen synchronisierten Abschnitt dieses Objekts ausführt, alle anderen synchronisierten Abschnitte des Objekts für andere Threads unzugänglich. Wenn also Thread T1 die synchronisierte Methode F.increment ausführt, kann Thread T2 nicht nur F.increment, sondern auch jeden anderen synchronisierten Abschnitt von F nicht ausführen, selbst wenn kein anderer Thread diesen nutzt. Dies ist daher eine restriktivere Methode.

Nehmen wir also an, dass das Fenster zur kritischen Ressource wird. Wir würden dann schreiben:

   // increment
  private void incrémente(){
     // review section
    synchronized(this){
       // retrieve the thread number
      int iThread=0;
      try{
        iThread=Integer.parseInt(Thread.currentThread().getName());
      }catch(Exception ex){}
       // read the job counter value
      try{
        compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
      } catch (Exception e){}
       // increment it
      compteurs[iThread]++;

       // wait 100 milliseconds - the thread will then lose the processor
      try{
        Thread.sleep(100);
      } catch (Exception e){
        System.exit(0);
      }

       // the new counter is displayed
      txtGénérés.setText("");
      txtGénérés.setText(""+compteurs[iThread]);
    }//synchronized
  }// increment

Alle Threads verwenden das this-Fenster zur Synchronisation. Zur Laufzeit erhalten wir dieselben korrekten Ergebnisse wie zuvor. Wir können tatsächlich auf jedem Objekt synchronisieren, das allen Threads bekannt ist. Hier ist zum Beispiel eine weitere Methode, die zu denselben Ergebnissen führt:

   // instance variables
    Thread[] tâches=null;   // threads
    int[] compteurs=null;   // meters
    Object synchro=new Object(); // a thread synchronization object

   // increment
  private void incrémente(){
     // review section
    synchronized(synchro){
..............
    }//synchronized
  }// increment

Das Fenster erstellt ein Objekt vom Typ „Object“, das für die Thread-Synchronisation verwendet wird. Diese Methode ist besser als diejenige, die auf dem this-Objekt synchronisiert, da sie weniger restriktiv ist. Befindet sich hier ein Thread T1 im synchronisierten Abschnitt von increment und möchte ein Thread T2 einen anderen synchronisierten Abschnitt desselben this-Objekts ausführen, 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 canPass, um einem Thread mitzuteilen, ob er einen kritischen Abschnitt betreten darf oder nicht. Eine Version ohne Synchronisation könnte wie folgt aussehen:


while(! peutPasser);        // on attend que peutPasser passe à vrai
peutPasser=false;            // aucun autre thread ne doit passer
section critique;            // ici le thread est tout seul
peutPasser=true;            // un autre thread peut passer dans la section critique

Die erste Anweisung, bei der ein Thread in einer Schleife wartet, bis canPass wahr wird, ist ineffizient: Der Thread belegt unnötig den Prozessor. Dies wird als aktives Warten bezeichnet. Wir können den Code wie folgt verbessern:

while(! peutPasser){        // wait for peutPasser to change to true
   Thread.sleep(100);    // off for 100 ms
}
peutPasser=false;            // no other thread may pass
section critique;            // here the thread is all alone
peutPasser=true;            // another thread can enter the critical section

Die Warte-Schleife ist hier besser: Wenn der Thread nicht passieren kann, ruht er 100 ms lang, bevor er erneut prüft, ob er passieren kann oder nicht. In der Zwischenzeit wird der Prozessor anderen Threads im System zugewiesen.

Beide Methoden sind eigentlich falsch: Sie verhindern nicht, dass zwei Threads gleichzeitig in den kritischen Abschnitt eintreten. Angenommen, ein Thread T1 stellt fest, dass canPass wahr ist. Er fährt dann mit der nächsten Anweisung fort, in der er canPass auf falsch setzt, um die anderen Threads zu blockieren. Es kann jedoch durchaus sein, dass er in diesem Moment unterbrochen wird, entweder weil seine Prozessor-Zeitscheibe 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 wenig später wiedererlangen. In der Zwischenzeit erhalten andere Aufgaben den Prozessor, darunter möglicherweise ein Thread T2, der in einer Schleife wartet, bis peutPasser wahr wird. Auch er wird feststellen, dass peutPasser wahr ist (der erste Thread hatte keine Zeit, es auf „false“ zu setzen) und wird ebenfalls in den kritischen Abschnitt eintreten. Was eigentlich nicht passieren sollte.

Die Abfolge


while(! peutPasser){            // wait for peutPasser to change to true
   try{
        Thread.sleep(100);    // off for 100 ms
    } catch (Exception e) {}
}// while
peutPasser=false;                // no other thread may pass

ist ein kritischer Abschnitt, der durch Synchronisation geschützt werden muss. Basierend auf dem vorherigen Beispiel können wir schreiben:


    synchronized(synchro){
        while(! peutPasser){            // wait for peutPasser to change to true
            try{
                Thread.sleep(100);    // off for 100 ms
            } catch (Exception e) {}
        }//while
        peutPasser=false;                // no other thread may pass
    }// synchronized
    section critique;                    // here the thread is all alone
    peutPasser=true;                    // another thread can enter the critical section

Dieses Beispiel funktioniert korrekt. Wir können es verbessern, indem wir das semi-aktive Warten des Threads vermeiden, während er regelmäßig den Wert der booleschen Variable canPass überprüft. Anstatt alle 100 ms aufzuwachen, um den Status von canPass zu überprüfen, kann er in den Ruhezustand wechseln und anfordern, geweckt zu werden, wenn canPass wahr ist. Wir schreiben dies wie folgt:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // if we can't get through then we wait
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // no other thread may pass
}// synchronized

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


synchronized(synchro){

}// synchronized

das stellt sicher, dass der Thread Eigentümer des Synchro-Objekts ist. Durch den Aufruf von synchro.wait() gibt der Thread die Eigentümerschaft an der Synchronisationssperre ab. Warum ist das so? Im Allgemeinen, weil ihm die Ressourcen fehlen, um weiterzuarbeiten. Anstatt also andere Threads zu blockieren, die auf die Synchro-Ressource warten, gibt er sie frei und wartet auf die Ressource, die ihm fehlt. In unserem Beispiel wartet er darauf, dass der boolesche Wert canPass auf true gesetzt wird. Wie wird er über dieses Ereignis benachrichtigt? Wie folgt:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // if we can't get through then we wait
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // no other thread may pass
}// synchronized
section critique...
synchronized(synchro){
      synchro.notify();
    }

Betrachten wir den ersten Thread, der die Synchronisationssperre erwirbt. Nennen wir ihn T1. Angenommen, er findet den booleschen Wert canPass als wahr vor, da er der erste Thread ist. Er setzt ihn dann auf falsch. Anschließend verlässt er den durch das Sync-Objekt gesperrten kritischen Abschnitt. Ein anderer Thread kann nun den kritischen Abschnitt betreten, um den Wert von canPass zu überprüfen. Er wird feststellen, dass dieser auf „false“ steht, und dann auf ein Ereignis warten (wait). Dabei gibt er die Kontrolle über das Sync-Objekt ab. Ein weiterer Thread kann nun den kritischen Abschnitt betreten: Auch er wird warten, da canPass auf „false“ steht. Es können also mehrere Threads auf ein Ereignis am Sync-Objekt warten.

Kehren wir zu Thread T1 zurück, dem Zugriff gewährt wurde. Er führt den kritischen Abschnitt aus und signalisiert dann, dass ein anderer Thread nun fortfahren darf. Dies geschieht in der folgenden Reihenfolge:


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

Er muss zunächst mithilfe der synchronized-Anweisung wieder die Kontrolle über das sync-Objekt erlangen. Dies sollte kein Problem darstellen, da er mit Threads konkurriert, die, falls sie das sync-Objekt vorübergehend erhalten, es über einen wait-Befehl wieder freigeben müssen, da sie feststellen, dass peutPasser false ist. Unser Thread T1 wird also schließlich die Kontrolle über das Synchro-Objekt erlangen. Sobald dies geschehen ist, signalisiert er über die Operation synchro.notify*, dass einer der durch *synchro.wait* blockierten Threads geweckt werden muss. Anschließend gibt er die Kontrolle über das Synchro-Objekt wieder ab, das dann an einen der wartenden Threads übergeben wird. Dieser Thread setzt seine Ausführung mit der Anweisung fort, die auf das wait folgt, das ihn angehalten hatte. Im Gegenzug führt er den kritischen Abschnitt aus und gibt ein synchro.notify* aus, um einen anderen Thread freizugeben. Und so weiter.

Schauen wir uns anhand des bereits behandelten Zählbeispiels an, wie dies funktioniert.

  void btnGénérer_actionPerformed(ActionEvent e) {
     //thread generation

     // read the number of threads to generate
    int nbThreads=0;
    try{
      // read the field containing the number of threads
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      // positive >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //error
      txtStatus.setText("Nombre invalide");
       // we start again
      txtAGénérer.requestFocus();
      return;
    }//catch

     // RAZ job counter
    txtGénérés.setText("0");  // task cpteur at 0
    // 1st thread can pass
    peutPasser=true;
     // threads are generated and launched
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // create thread i
      tâches[i]=new Thread() {
          public void run() {
          synchronise();
        }
      };//thread i
       // define its name
      tâches[i].setName(""+i);
      // start execution
      tâches[i].start();
    }//for
  }//generate

Nun führen die Threads nicht mehr die Increment-Methode aus, sondern die folgende Synchronize-Methode:

   // thread synchronization step
  public void synchronise(){
     // request access to the critical section
    synchronized(synchro){
      try{
        // can we get through?
        if(! peutPasser){
          System.out.println(Thread.currentThread().getName()+ " en attente");
          synchro.wait();
        }
         // we have passed - we forbid other threads to pass
        peutPasser=false;
      } catch(Exception e){
        txtStatus.setText(""+e);
                    return;
      }//try
    }// synchronized

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

     // we're done - we release any thread blocked at the entrance to the critical section
    peutPasser=true;
    System.out.println(Thread.currentThread().getName()+ " terminé");
    synchronized(synchro){
      synchro.notify();
    }// synchronized
  } // synchronize

Der Zweck der Methode synchronize besteht darin, Threads nacheinander zu verarbeiten. Dazu verwendet sie eine Synchronisationsvariable namens synchro. Die Methode increment wird nicht mehr durch das Schlüsselwort synchronized geschützt:

   // increment
  private void incrémente(){
     // retrieve the thread number
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // read the job counter value
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // increment it
    compteurs[iThread]++;
     // wait 100 milliseconds - the thread will then lose the processor
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }
     // the new counter is displayed
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
  }// increment

Bei 5 Threads lauten die Ergebnisse wie folgt:

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 fünf von der Anwendung generierten Threads. T0 erlangt als erster die Sync-Sperre und stellt fest, dass peutPasser auf „true“ gesetzt ist. Er setzt peutPasser auf „false“ und fährt fort: Dies ist der Zweck der ersten gesendeten Nachricht. Aller Wahrscheinlichkeit nach fährt er fort und führt den kritischen Abschnitt aus, insbesondere die Methode increment. In dieser Methode wird er für 100 ms in den Ruhezustand versetzt (sleep). Er gibt somit den Prozessor frei. Der Prozessor wird dann einem anderen Thread, Thread T1, zugewiesen, der daraufhin die Kontrolle über das Sync-Objekt erlangt. Er stellt fest, dass er nicht fortfahren kann und wechselt in einen Wartezustand (wait). Anschließend gibt er die Kontrolle über das Sync-Objekt sowie den Prozessor frei. Der Prozessor wird Thread T2 zugewiesen, dem dasselbe Schicksal widerfährt. Während der 100 ms, in denen T0 pausiert, werden die Threads T1 bis T4 daher in die Warteschlange gestellt. Das ist die Bedeutung der 4 „waiting“-Meldungen. Nach 100 ms erhält T0 den Prozessor zurück und schließt seine Arbeit ab: Das ist die Bedeutung der Meldung „0 finished“. Er gibt dann einen der blockierten Threads frei und beendet sich. Der freigewordene Prozessor wird dann einem verfügbaren Thread zugewiesen: demjenigen, der gerade freigegeben wurde. Hier ist es T1. Thread T1 betritt dann den kritischen Abschnitt: Das ist die Bedeutung der Meldung „1 passed“. Er erledigt, was er zu tun hat, und pausiert dann für 100 ms. Der Prozessor steht dann für einen anderen Thread zur Verfügung, aber alle warten auf ein Ereignis: Keiner von ihnen kann den Prozessor übernehmen. Nach 100 ms beansprucht Thread T1 den Prozessor erneut und beendet sich: Das ist die Bedeutung der Meldung „1 finished“. Die Threads T1 bis T4 verhalten sich genauso wie T1: Das ist die Bedeutung der drei Meldungsreihen: „passed“, „finished“.