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:
gibt den aktuell ausgeführten Thread zurück | |
legt den Namen des Threads fest | |
Thread-Name | |
gibt an, ob der Thread aktiv ist (true) oder nicht (false) | |
startet die Ausführung eines Threads | |
Methode, die automatisch ausgeführt wird, nachdem die vorangehende start-Methode ausgeführt wurde | |
unterbricht die Ausführung eines Threads für n Millisekunden | |
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:
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. | |
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:
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
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:
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.

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.

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:
![]() |
|
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:
Die run-Methode des Threads ruft die runHorloge-Methode der Anwendung auf. Sobald dies geschehen ist, wird der Thread gestartet:
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:
- Zeigt die aktuelle Uhrzeit im Textfeld „txtHorloge“ an
- hält 1 Sekunde lang an
- 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:

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:

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:

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:
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:
Nun deklarieren wir sie anders:
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:

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:
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“.



