7. Uitvoeringsthreads
7.1. Inleiding
Wanneer een applicatie wordt gestart, wordt deze uitgevoerd in een uitvoeringsstroom die een thread wordt genoemd. De klasse die een thread modelleert, is de klasse java.lang.Thread, waarvan hier enkele eigenschappen en methoden worden weergegeven:
geeft de thread weer die momenteel wordt uitgevoerd | |
stelt de naam van de thread in | |
naam van de thread | |
geeft aan of de thread actief is (true) of niet (false) | |
start de uitvoering van een thread | |
methode die automatisch wordt uitgevoerd nadat de voorgaande start-methode is uitgevoerd | |
stopt de uitvoering van een thread gedurende n milliseconden | |
blokkerende bewerking – wacht tot de thread is voltooid om door te gaan naar de volgende instructie |
De meest gebruikte constructors zijn de volgende:
maakt een verwijzing naar een asynchrone taak. Deze is nog inactief. De aangemaakte taak moet de methode run hebben: meestal zal een klasse worden gebruikt die is afgeleid van Thread. | |
Hetzelfde, maar het is het als parameter doorgegeven object Runnable dat de methode run implementeert. |
Laten we eens kijken naar een eerste toepassing die het bestaan van een hoofduitvoeringsthread laat zien, namelijk degene waarin de functie main van een klasse wordt uitgevoerd:
// gebruik van threads
import java.io.*;
import java.util.*;
public class thread1{
public static void main(String[] arg)throws Exception {
// huidige thread initialiseren
Thread main=Thread.currentThread();
// weergave
System.out.println("Thread courant : " + main.getName());
// de naam wordt gewijzigd
main.setName("myMainThread");
// controle
System.out.println("Thread courant : " + main.getName());
// oneindige lus
while(true){
// de tijd ophalen
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// weergave
System.out.println(main.getName() + " : " +H);
// tijdelijk stoppen
Thread.sleep(1000);
}//while
}//main
}//klasse
De schermresultaten:
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
Het voorgaande voorbeeld illustreert de volgende punten:
- de functie main wordt correct uitgevoerd in een thread
- men heeft toegang tot de kenmerken van deze thread via Thread.currentThread()
- de rol van de methode sleep. Hier gaat de thread die main uitvoert regelmatig 1 seconde in slaapstand tussen twee weergaven.
7.2. Aanmaken van uitvoeringsthreads
Er zijn toepassingen mogelijk waarbij stukken code „gelijktijdig“ in verschillende uitvoeringsthreads worden uitgevoerd. Wanneer men zegt dat thread'en gelijktijdig worden uitgevoerd, is dat vaak een verkeerde benaming. Als de machine slechts één processor heeft, zoals nog vaak het geval is, delen de thread'en deze processor: ze beschikken er om de beurt gedurende een kort moment (enkele milliseconden) over. Dit wekt de illusie van parallelle uitvoering. De tijd die aan een thread wordt toegewezen, hangt af van verschillende factoren, waaronder de prioriteit ervan, die een standaardwaarde heeft maar ook programmatisch kan worden ingesteld. Wanneer een thread de processor tot zijn beschikking heeft, gebruikt hij deze normaal gesproken gedurende de volledige tijd die hem is toegewezen. Hij kan deze echter ook vroegtijdig vrijgeven:
- door te wachten op een gebeurtenis (wait, join)
- door gedurende een bepaalde tijd in slaapstand te gaan (sleep)
- Een thread T kan op verschillende manieren worden aangemaakt
- door de klasse Thread te erven en de methode run daarvan te herdefiniëren.
- door de interface Runnable in een klasse te implementeren en de constructor new Thread(Runnable) te gebruiken. Runnable is een interface die slechts één methode definieert: public void run(). Het argument van de bovenstaande constructor is dus elk exemplaar van een klasse die deze methode run implementeert.
In het volgende voorbeeld worden threads aangemaakt met behulp van een anonieme klasse die is afgeleid van de klasse Thread:
// we maken de thread i aan
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//taken definiëren[i]
De methode run verwijst hier alleen maar door naar de methode affiche.
- De uitvoering van thread T wordt gestart door T.start(): deze methode behoort tot de klasse Thread en voert een aantal initialisaties uit, waarna automatisch de methode run van de thread of de interface Runnable wordt gestart. Het programma dat de instructie T.start() uitvoert, wacht niet tot taak T is voltooid: het gaat onmiddellijk door naar de volgende instructie. Er zijn dan twee taken die parallel worden uitgevoerd. Deze moeten vaak met elkaar kunnen communiceren om te weten hoe ver het gezamenlijke werk is gevorderd. Dit is het probleem van de synchronisatie van threads.
- Eenmaal gestart, wordt de thread autonoom uitgevoerd. Deze stopt wanneer de functie run die hij uitvoert, zijn werk heeft voltooid.
- We kunnen wachten tot de uitvoering van thread T door T.join() is voltooid. Dit is een blokkerende instructie: het programma dat deze uitvoert, wordt geblokkeerd totdat taak T zijn werk heeft voltooid. Dit is ook een manier van synchronisatie.
Laten we het volgende programma eens bekijken:
// gebruik van threads
import java.io.*;
import java.util.*;
public class thread2{
public static void main(String[] arg) {
// de huidige thread initialiseren
Thread main=Thread.currentThread();
// de huidige thread een naam geven
main.setName("myMainThread");
// begin van main
System.out.println("début du thread " +main.getName());
// uitvoeringsthreads aanmaken
Thread[] tâches=new Thread[5];
for(int i=0;i<tâches.length;i++){
// thread i aanmaken
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//taken definiëren[i]
// de naam van de thread wordt vastgelegd
tâches[i].setName(""+i);
// de thread i wordt gestart
tâches[i].start();
}//for
// einde van main
System.out.println("fin du thread " +main.getName());
}//Main
public static void affiche() {
// de tijd ophalen
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// weergave van het begin van de uitvoering
System.out.println("Début d'exécution de la méthode affiche dans le Thread " +
Thread.currentThread().getName()+ " : " + H);
// 1 seconde in slaapstand
try{
Thread.sleep(1000);
}catch (Exception ex){}
// de tijd ophalen
calendrier=Calendar.getInstance();
H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// weergave einde uitvoering
System.out.println("Fin d'exécution de la méthode affiche dans le Thread "
+Thread.currentThread().getName()+ " : " + H);
}// weergeven
}//klasse
De hoofdthread, die de functie main uitvoert, maakt 5 andere threads aan die de statische methode affiche moeten uitvoeren. De resultaten zijn als volgt:
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
Deze resultaten zijn zeer leerzaam:
- allereerst zien we dat het starten van een thread geen blokkering veroorzaakt. De methode main heeft de uitvoering van 5 threads parallel gestart en was zelf al klaar voordat de threads klaar waren. De bewerking
start de uitvoering van de thread tâches[i], maar zodra dit is gebeurd, gaat de uitvoering onmiddellijk verder met de volgende instructie, zonder te wachten tot de thread is voltooid.
- Alle aangemaakte threads moeten de methode affiche uitvoeren. De uitvoeringsvolgorde is onvoorspelbaar. Ook al lijkt de uitvoeringsvolgorde in het voorbeeld de volgorde van het starten van de threads te volgen, hieruit kunnen geen algemene conclusies worden getrokken. Het besturingssysteem beschikt hier over 6 threads en één processor. Het zal de processor over deze 6 threads verdelen volgens zijn eigen regels.
- In de resultaten zien we een gevolg van de methode sleep. In het voorbeeld is het thread 0 die als eerste de methode affiche uitvoert. Het bericht dat de uitvoering begint, wordt weergegeven, waarna de methode sleep wordt uitgevoerd, die de thread gedurende 1 seconde opschort. De thread verliest dan de processor, die daardoor beschikbaar komt voor een andere thread. Het voorbeeld laat zien dat thread 1 de processor krijgt. Thread 1 volgt hetzelfde traject, net als de andere threads. Wanneer de slaapperiode van 1 seconde van thread 0 is verstreken, kan de uitvoering ervan worden hervat. Het systeem wijst de processor aan deze thread toe en deze kan de uitvoering van de methode affiche voltooien.
Laten we ons programma aanpassen om de methode main af te sluiten met de instructies:
// einde van de hoofdprocedure
System.out.println("fin du thread " +main.getName());
// de applicatie wordt gestopt
System.exit(0);
De uitvoering van het nieuwe programma levert het volgende op:
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
Zodra de methode main de instructie uitvoert:
stopt ze alle threads van de applicatie en niet alleen de thread main. De methode main zou kunnen willen wachten tot de threads die zij heeft aangemaakt, zijn voltooid, voordat zij zelf wordt beëindigd. Dit kan worden gedaan met de methode join van de klasse Thread:
// wachten op alle threads
for(int i=0;i<tâches.length;i++){
// wachten op thread i
tâches[i].join();
}//for
// einde van de hoofdroutine
System.out.println("fin du thread " +main.getName());
// toepassing gestopt
System.exit(0);
Dit levert dan de volgende resultaten op:
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. Het belang van threads
Nu we hebben aangetoond dat er standaard één thread bestaat – namelijk degene die de methode Main uitvoert – en nu we weten hoe we andere threads kunnen aanmaken, gaan we even stilstaan bij het nut van threads voor ons en bij de reden waarom we ze hier behandelen. Er is een soort applicaties die zich goed lenen voor het gebruik van threads, namelijk client-server-applicaties op het internet. In zo’n applicatie beantwoordt een server op een machine S1 verzoeken van clients op externe machines C1, C2, ..., Cn.

We maken dagelijks gebruik van internettoepassingen die aan dit schema voldoen: webdiensten, e-mail, het raadplegen van forums, het overzetten van bestanden... In het bovenstaande schema moet de server S1 de Ci-clients gelijktijdig bedienen. Als we het voorbeeld nemen van een server FTP (File Transfer Protocol) die bestanden aan zijn clients levert, weten we dat een bestandsoverdracht soms meerdere uren kan duren. Het is natuurlijk uitgesloten dat één client de server zo lang in zijn eentje monopoliseert. Wat gewoonlijk gebeurt, is dat de server evenveel uitvoeringsthreads aanmaakt als er clients zijn. Elke thread is dan verantwoordelijk voor een specifieke client. Aangezien de processor cyclisch wordt verdeeld over alle actieve threads van de machine, besteedt de server een beetje tijd aan elke client, waardoor de gelijktijdigheid van de dienstverlening wordt gewaarborgd.

7.4. Een grafische klok
Laten we eens kijken naar de volgende toepassing die een venster weergeeft met een klok en een knop om de klok te stoppen of opnieuw te starten:
![]() | ![]() | ![]() |
Om de klok te laten lopen, moet een proces ervoor zorgen dat de tijd elke seconde wordt bijgewerkt. Tegelijkertijd moeten de gebeurtenissen in het venster worden gecontroleerd: wanneer de gebruiker op de knop "Stoppen" klikt, moet de klok worden gestopt. Dit zijn twee parallelle en asynchrone taken: de gebruiker kan op elk moment klikken.
Laten we eens kijken naar het moment waarop de klok nog niet is gestart en de gebruiker op de knop "Starten" klikt. Dit is een klassieke gebeurtenis en je zou kunnen denken dat een methode van de thread waarin het venster wordt uitgevoerd, de klok dan kan beheren. Maar wanneer een methode van de grafische applicatie wordt uitgevoerd, luistert de thread van die applicatie niet meer naar gebeurtenissen van de grafische interface. Deze gebeurtenissen vinden plaats en worden in een wachtrij geplaatst om door de applicatie te worden verwerkt zodra de methode die op dat moment wordt uitgevoerd, is voltooid. In ons voorbeeld van de klok zal de methode altijd actief zijn, aangezien deze alleen kan worden gestopt door op de knop „Stoppen“ te klikken. Dit gebeurtenis wordt echter pas verwerkt wanneer de methode is voltooid. We zitten in een vicieuze cirkel.
De oplossing voor dit probleem zou zijn dat, wanneer de gebruiker op de knop "Starten" klikt, er een taak wordt gestart om de klok te beheren, maar dat de applicatie kan blijven luisteren naar de gebeurtenissen die zich in het venster voordoen. We zouden dan twee afzonderlijke taken hebben die parallel worden uitgevoerd:
- beheer van de klok
- het luisteren naar gebeurtenissen in het venster
Laten we teruggaan naar onze grafische klok:
![]() |
|
De bruikbare code van de met JBuilder gebouwde applicatie is als volgt:
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();
// instantie-attributen
boolean finHorloge=true;
//Het frame opbouwen
public interfaceHorloge() {
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
try {
jbInit();
}
catch(Exception e) {
e.printStackTrace();
}
}
private void runHorloge(){
// we blijven herhalen totdat ons wordt verteld te stoppen
while( ! finHorloge){
// we halen de tijd op
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// we geven deze weer in veld T
txtHorloge.setText(H);
// één seconde wachten
try{
Thread.sleep(1000);
} catch (Exception e){
// uitvoer met fout
System.exit(1);
}//try
}// while
}// runHorloge
//Component initialiseren
private void jbInit() throws Exception {
...................
}
//Vervangen, zodat we kunnen afsluiten wanneer het venster wordt gesloten
protected void processWindowEvent(WindowEvent e) {
.............
}
void btnGoStop_actionPerformed(ActionEvent e) {
// de klok starten/stoppen
// de tekst van de knop ophalen
String libellé=btnGoStop.getText();
// starten?
if(libellé.equals("Lancer")){
// we maken de thread aan waarin de klok zal draaien
Thread thHorloge=new Thread(){
public void run(){
runHorloge();
}
};//thread definiëren
// we geven de thread toestemming om te starten
finHorloge=false;
// de tekst van de knop wijzigen
btnGoStop.setText("Arrêter");
// de thread wordt gestart
thHorloge.start();
// einde
return;
}//if
// stoppen
if(libellé.equals("Arrêter")){
// de thread wordt gevraagd te stoppen
finHorloge=true;
// de tekst van de knop wordt gewijzigd
btnGoStop.setText("Lancer");
// einde
return;
}//if
}
}
Wanneer de gebruiker op de knop "Starten" klikt, wordt er een thread aangemaakt met behulp van een anonieme klasse:
De methode run van de thread verwijst naar de methode runHorloge van de applicatie. Zodra dit is gebeurd, wordt de thread gestart:
De methode runHorloge wordt vervolgens uitgevoerd:
private void runHorloge(){
// we blijven herhalen totdat ons wordt verteld te stoppen
while( ! finHorloge){
// we halen de tijd op
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// we geven deze weer in veld T
txtHorloge.setText(H);
// een seconde wachten
try{
Thread.sleep(1000);
} catch (Exception e){
// uitvoer met fout
System.exit(1);
}//probeer
}// while
}// runHorloge
Het principe van de methode is als volgt:
- de huidige tijd wordt weergegeven in het tekstvak txtHorloge
- wacht 1 seconde
- herhaalt stap 1, waarbij eerst de booleaanse variabele finHorloge wordt gecontroleerd; deze wordt op ‘waar’ gezet wanneer de gebruiker op de knop Arrêter klikt.
7.5. Klok-applet
We zetten de vorige grafische toepassing op de gebruikelijke manier om in een applet en maken het volgende document 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>
Wanneer we dit document rechtstreeks in IE laden door erop te dubbelklikken, krijgen we de volgende weergave:

Alle elementen die de applet nodig heeft, bevinden zich in dit voorbeeld in dezelfde map:
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
Onze applet kan worden verbeterd. We hebben gezegd dat bij het laden van de applet de methode init wordt uitgevoerd en vervolgens de methode start, indien deze bestaat. Bovendien wordt, wanneer de gebruiker de pagina verlaat, de methode stop uitgevoerd, indien deze bestaat. Wanneer hij terugkeert naar de pagina van de applet, wordt de methode start opnieuw aangeroepen. Wanneer een applet threads voor visuele animatie gebruikt, worden vaak de methoden start en stop van de applet gebruikt om de threads te starten en te stoppen. Het heeft namelijk geen zin dat een thread voor visuele animatie op de achtergrond blijft werken terwijl de animatie verborgen is.
Daarom voegen we de volgende methoden start en stop toe aan onze applet:
public void stop(){
// de pagina is verborgen
// vervolg
System.out.println("Page stop");
// de pagina is verborgen – de thread wordt gestopt
finHorloge=true;
}
public void start(){
// de pagina verschijnt weer
// vervolg
System.out.println("Page start");
// indien nodig wordt een nieuwe klokthread gestart
if(btnGoStop.getText().equals("Arrêter")){
// de tekst wordt gewijzigd
btnGoStop.setText("Lancer");
// en we doen alsof de gebruiker erop heeft geklikt
btnGoStop_actionPerformed(null);
}//if
}//start
Daarnaast hebben we in de methode run van de thread een controle toegevoegd om te zien wanneer deze start en stopt:
private void runHorloge(){
// gevolgd door
System.out.println("Thread horloge lancé");
// we herhalen dit zolang ons niet wordt verteld te stoppen
while( ! finHorloge){
// we halen de tijd op
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// we geven deze weer in veld T
txtHorloge.setText(H);
// één seconde wachten
try{
Thread.sleep(1000);
} catch (Exception e){
// uitvoer met fout
return;
}//try
}// while
// vervolg
System.out.println("Thread horloge terminé");
}// runHorloge
Nu voeren we de applet uit met AppletViewer:
E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start // applet gestart - pagina weergegeven
Thread horloge lancé // de thread wordt dienovereenkomstig gestart
Page stop // applet is geminimaliseerd
Thread horloge terminé // de thread is dienovereenkomstig gestopt
Page start // applet opnieuw weergegeven
Thread horloge lancé // de thread wordt opnieuw gestart
Thread horloge terminé // op de stopknop gedrukt
Thread horloge lancé // op de startknop gedrukt
Page stop // applet als pictogram e
Thread horloge terminé // thread dienovereenkomstig gestopt
Page start // applet opnieuw weergegeven
Thread horloge lancé // thread dienovereenkomstig opnieuw gestart
Bij AppletViewer vindt de gebeurtenis start plaats wanneer het venster van AppletViewer zichtbaar is, en de gebeurtenis stop wanneer het venster naar de taakbalk wordt verplaatst. Uit bovenstaande resultaten blijkt dat wanneer het document HTML wordt verborgen, de thread inderdaad wordt gestopt als deze actief was.
7.6. Synchronisatie van taken
In ons vorige voorbeeld waren er twee taken:
- de hoofdtaak, vertegenwoordigd door de applicatie zelf
- de taak die verantwoordelijk was voor de klok
De coördinatie tussen de twee taken werd verzorgd door de hoofdtaak, die een booleaanse variabele instelde om de klokthread te stoppen. We gaan nu in op het probleem van gelijktijdige toegang van taken tot gemeenschappelijke bronnen, een probleem dat ook wel bekend staat als "het delen van bronnen". Om dit te illustreren, zullen we eerst een voorbeeld bekijken.
7.6.1. Een niet-gesynchroniseerde telling
Laten we eens kijken naar de volgende grafische interface:

nr. | type | naam | rol |
1 | JTextField | txtAGénérer | geeft het aantal te genereren threads aan |
2 | JTextfield (niet bewerkbaar) | txtGénéres | geeft het aantal gegenereerde threads aan |
3 | JTextField (niet bewerkbaar) | txtStatus | geeft informatie over de opgetreden fouten en over de applicatie zelf |
4 | JButton | btnGénérer | start het genereren van threads |
De applicatie werkt als volgt:
- de gebruiker geeft in veld 1 het aantal te genereren threads op
- hij start het genereren van deze threads met knop 4
- de threads lezen de waarde van veld 2, verhogen deze en geven de nieuwe waarde weer. In het begin bevat dit veld de waarde 0.
De gegenereerde threads delen één bron: de waarde van veld 2. We willen hier de problemen laten zien die in een dergelijke situatie kunnen optreden. Hier volgt een voorbeeld van de uitvoering:

We zien dat er 1000 threads moesten worden gegenereerd, maar dat er slechts 7 zijn geteld. De relevante code van de applicatie is als volgt:
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();
// instantievariabelen
Thread[] tâches=null; // de threads
int[] compteurs=null; // de tellers
//Het frame opbouwen
public interfaceSynchro() {
..........
}
//De component initialiseren
private void jbInit() throws Exception {
......................
}
//Vervangen, zodat we kunnen afsluiten wanneer het venster wordt gesloten
protected void processWindowEvent(WindowEvent e) {
..................
}
void btnGénérer_actionPerformed(ActionEvent e) {
//het genereren van threads
// het aantal te genereren threads wordt gelezen
int nbThreads=0;
try{
// het veld met het aantal threads uitlezen
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// positief >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//fout
txtStatus.setText("Nombre invalide");
// opnieuw beginnen
txtAGénérer.requestFocus();
return;
}//catch
// in het begin geen threads gegenereerd
txtGénérés.setText("0"); // taakteller op 0
// threads worden aangemaakt en gestart
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// thread i wordt aangemaakt
tâches[i]=new Thread() {
public void run() {
incrémente();
}
};//thread i
// de naam ervan wordt gedefinieerd
tâches[i].setName(""+i);
// de uitvoering wordt gestart
tâches[i].start();
}//for
}//genereren
// verhogen
private void incrémente(){
// het threadnummer ophalen
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// de waarde van de taakteller wordt gelezen
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// deze wordt verhoogd
compteurs[iThread]++;
// 100 milliseconden wachten – de thread verliest dan de processor
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// de nieuwe tellerstand wordt weergegeven
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
// vervolg
System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
}// verhoogt
}// klasse
Laten we de code eens nader bekijken:
- het venster declareert twee instantievariabelen:
De array tâches is de array van de gegenereerde threads. De array compteurs wordt gekoppeld aan de array tâches. Elke taak heeft een eigen teller om de waarde van het veld txtGénérés uit de grafische interface op te halen.
- Wanneer er op de knop Générer wordt geklikt, wordt de methode btnGénérer_actionPerformed uitgevoerd.
- Deze methode begint met het ophalen van het aantal te genereren threads. Indien nodig wordt er een fout gemeld als dit aantal niet bruikbaar is. Vervolgens genereert de methode de gevraagde threads, waarbij de referenties ervan in een array worden opgeslagen en aan elke thread een nummer wordt toegekend. De methode run van de gegenereerde threads verwijst door naar de methode incrémente van de klasse. Alle threads worden gestart (start). Ook wordt de array met tellers die aan de threads zijn gekoppeld, aangemaakt.
- de methode incrémente:
- leest de huidige waarde van het veld txtGénérés en slaat deze op in de teller die bij de momenteel actieve thread hoort
- wacht 100 ms, om de processor opzettelijk vrij te geven
- geeft de nieuwe waarde weer in het veld txtGénérés
Laten we nu uitleggen waarom de telling van de threads onjuist is. Stel dat er 2 threads moeten worden gegenereerd. Deze worden in een onvoorspelbare volgorde uitgevoerd. Een van beide komt als eerste aan de beurt en leest de waarde 0 uit de teller. Hij zet deze vervolgens op 1, maar schrijft de waarde niet in het venster: hij onderbreekt zichzelf opzettelijk gedurende 100 ms. Hij verliest dan de processor, die vervolgens aan een andere thread wordt toegewezen. Deze werkt op dezelfde manier als de vorige: hij leest de teller van het venster en haalt de 0 op die er nog steeds staat. Hij zet de teller op 1 en, net als de vorige, onderbreekt hij zichzelf gedurende 100 ms. De processor wordt vervolgens weer toegewezen aan de eerste thread: deze schrijft de waarde 1 in de teller van het venster en beëindigt zichzelf. De processor wordt nu toegewezen aan de tweede thread, die ook 1 schrijft. We krijgen een onjuist resultaat.
Waar komt het probleem vandaan? De tweede thread heeft een verkeerde waarde gelezen omdat de eerste thread was onderbroken voordat hij zijn taak – het bijwerken van de teller in het venster – had voltooid. Dit brengt ons bij het begrip ‘kritieke resource’ en ‘kritieke sectie’ van een programma:
- een kritieke resource is een resource die slechts door één thread tegelijk kan worden bezet. In dit geval is de kritieke resource de teller 2 in het venster.
- Een kritieke sectie van een programma is een reeks instructies in de uitvoeringsstroom van een thread waarin deze toegang heeft tot een kritieke resource. Er moet worden gewaarborgd dat deze thread tijdens deze kritieke sectie als enige toegang heeft tot de resource.
7.6.2. Een via een methode gesynchroniseerde telling
In het vorige voorbeeld voerde elke thread de methode incrémente van het venster uit. De methode incrémente was als volgt gedeclareerd:
Nu declareren we deze anders:
Het sleutelwoord **synchronized* betekent dat slechts één thread tegelijk de methode *incrémente kan uitvoeren. Laten we de volgende notaties eens bekijken:
- het vensterobject F dat de threads aanmaakt in btnGénérer_actionPerformed
- twee threads, T1 en T2, die door F worden aangemaakt
Beide threads worden door F aangemaakt en vervolgens gestart. Ze zullen dus allebei de methode F.run uitvoeren. Stel dat T1 als eerste aankomt. Deze voert F.run uit en vervolgens F.incremente, wat een gesynchroniseerde methode is. Hij leest de waarde 0 van de teller, verhoogt deze en wacht vervolgens 100 ms. De processor wordt dan overgedragen aan T2, die op zijn beurt F.run uitvoert en vervolgens F.incremente. En daar loopt het vast, omdat de thread T1 op dat moment F.incremente uitvoert en het sleutelwoord synchronized ervoor zorgt dat slechts één thread tegelijk F.incremente kan uitvoeren. T2 verliest vervolgens op zijn beurt de processor zonder de waarde van de teller te hebben kunnen lezen. Na 100 ms krijgt T1 de processor weer toegewezen, geeft de waarde 1 van de teller weer, verlaat F.incremente en vervolgens F.run en wordt beëindigd. T2 krijgt vervolgens de processor terug en kan deze keer F.incremente uitvoeren, omdat T1 deze methode niet meer uitvoert. T2 leest vervolgens de waarde 1 van de teller, verhoogt deze met 1 en stopt gedurende 100 ms. Na 100 ms krijgt het de processor weer toegewezen, geeft de waarde 2 van de teller weer en wordt ook beëindigd. Deze keer is de verkregen waarde correct. Hier is een getest voorbeeld:

7.6.3. Door een object gesynchroniseerde telling
In het vorige voorbeeld werd de toegang tot de teller txtGénérés gesynchroniseerd door een methode. Als het venster dat de threads aanmaakt F heet, kunnen we ook zeggen dat de methode F.incremente een hulpbron vertegenwoordigt die slechts door één thread tegelijk mocht worden gebruikt. Het is dus een kritieke hulpbron. De gesynchroniseerde toegang tot deze hulpbron werd gegarandeerd door het sleutelwoord synchronized:
Je zou ook kunnen zeggen dat de kritieke hulpbron het object F zelf is. Dit is strenger dan in het geval waarin de kritieke hulpbron F.incremente is. In dat laatste geval geldt namelijk dat, als een thread T1 de methode F.incremente uitvoert, dan kan een thread T2 F.incremente niet uitvoeren, maar wel een andere methode van het object F, ongeacht of deze gesynchroniseerd is of niet. In het geval dat object F zelf de kritieke hulpbron is, wordt, wanneer een thread T1 een gesynchroniseerd gedeelte van dit object uitvoert, elk ander gesynchroniseerd gedeelte van het object ontoegankelijk voor de andere threads. Als een thread T1 dus de gesynchroniseerde methode F.incremente uitvoert, kan een thread T2 niet alleen F.incremente niet uitvoeren, maar ook geen enkel ander gesynchroniseerd gedeelte van F, zelfs als geen enkele thread deze gebruikt. Het is dus een methode die meer beperkingen oplegt.
Laten we dus aannemen dat het venster de kritieke bron wordt. We schrijven dan:
// incrementeren
private void incrémente(){
// kritieke sectie
synchronized(this){
// het threadnummer wordt opgehaald
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// de waarde van de taak-teller wordt gelezen
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// deze wordt verhoogd
compteurs[iThread]++;
// er wordt 100 milliseconden gewacht – de thread verliest dan de processor
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// de nieuwe tellerstand wordt weergegeven
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}//synchronized
}// verhoogt
Alle threads gebruiken het venster this om te synchroniseren. Bij uitvoering krijgt men dezelfde correcte resultaten als eerder. Men kan in feite synchroniseren op elk object dat bij alle threads bekend is. Hier is bijvoorbeeld een andere methode die dezelfde resultaten oplevert:
// instantievariabelen
Thread[] tâches=null; // de threads
int[] compteurs=null; // de tellers
Object synchro=new Object(); // een thread-synchronisatieobject
// verhogen
private void incrémente(){
// kritiek gedeelte
synchronized(synchro){
..............
}//gesynchroniseerd
}// verhoogt
Het venster maakt een object van het type Object aan dat zal worden gebruikt voor de synchronisatie van de threads. Deze methode is beter dan de methode die synchroniseert op het object this, omdat deze minder beperkend is. Stel dat een thread T1 zich in het gesynchroniseerde gedeelte van incrémente bevindt en dateen thread T2 een ander gesynchroniseerd gedeelte van hetzelfde object *this wil uitvoeren, maar dan gesynchroniseerd door een ander object dan synchro*, dan kan dat.
7.6.4. Synchronisatie op basis van gebeurtenissen
Deze keer gebruiken we een booleaanse variabele peutPasser om een thread aan te geven of deze al dan niet een kritieke sectie mag betreden. Een code zonder synchronisatie zou er als volgt uit kunnen zien:
while(! peutPasser); // we wachten tot peutPasser waar wordt
peutPasser=false; // er mag geen andere thread langskomen
section critique; // hier is de thread helemaal alleen
peutPasser=true; // een andere thread mag het kritieke gedeelte binnengaan
De eerste instructie, waarbij een thread in een lus blijft wachten tot peutPasser waar wordt, is onhandig: de thread houdt de processor onnodig bezet. Dit wordt ‘actief wachten’ genoemd. De code kan als volgt worden verbeterd:
while(! peutPasser){ // we wachten tot peutPasser waar wordt
Thread.sleep(100); // stop gedurende 100 ms
}
peutPasser=false; // er mogen geen andere threads doorkomen
section critique; // hier is de thread helemaal alleen
peutPasser=true; // een andere thread kan de kritieke sectie binnengaan
De wachtlus is hier beter: als de thread niet door kan, gaat hij 100 ms in slaapstand voordat hij opnieuw controleert of hij wel of niet door kan. De processor wordt ondertussen toegewezen aan andere threads van het systeem.
Deze twee methoden zijn in feite onjuist: ze voorkomen niet dat twee threads tegelijkertijd de kritieke sectie binnendringen. Stel dat een thread T1 detecteert dat peutPasser waar is. Hij gaat dan door naar de volgende instructie, waar hij peutPasser weer op ‘false’ zet om de andere threads te blokkeren. Alleen kan hij op dat moment heel goed worden onderbroken, hetzij omdat zijn procesortijd is opgebruikt, hetzij omdat een taak met hogere prioriteit de processor heeft aangevraagd, of om een andere reden. Het gevolg is dat hij de processor kwijtraakt. Hij krijgt deze iets later weer terug. Ondertussen krijgen andere taken de processor toegewezen, waaronder mogelijk een thread T2 die in een lus blijft hangen in afwachting dat peutPasser op ‘waar’ komt te staan. Ook deze thread zal ontdekken dat peutPasser op ‘true’ staat (de eerste thread heeft geen tijd gehad om deze op ‘false’ te zetten) en zal eveneens de kritieke sectie binnengaan. En dat had niet gemoeten.
De reeks
while(! peutPasser){ // we wachten tot peutPasser op 'waar' staat
try{
Thread.sleep(100); // stop gedurende 100 ms
} catch (Exception e) {}
}// while
peutPasser=false; // er mag geen andere thread doorkomen
is een kritieke reeks die door middel van synchronisatie moet worden beschermd. In navolging van het vorige voorbeeld kunnen we het volgende schrijven:
synchronized(synchro){
while(! peutPasser){ // er wordt gewacht tot peutPasser waar wordt
try{
Thread.sleep(100); // stop gedurende 100 ms
} catch (Exception e) {}
}//while
peutPasser=false; // er mag geen andere thread doorlopen
}// gesynchroniseerd
section critique; // hier is de thread helemaal alleen
peutPasser=true; // een andere thread mag de kritieke sectie passeren
Dit voorbeeld werkt correct. Het kan worden verbeterd door het semi-actieve wachten van de thread te vermijden wanneer deze regelmatig de waarde van de booleaanse variabele peutPasser controleert. In plaats van regelmatig om de 100 ms wakker te worden om de status van peutPasser te controleren, kan de thread in slaapstand gaan en vragen om gewekt te worden wanneer peutPasser op ‘waar’ staat. Dit schrijft men als volgt:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // als we er niet door kunnen, dan wachten we
} catch (Exception e){
…
}
}
peutPasser=false; // geen enkele andere thread mag erdoor
}// synchronized
De bewerking synchro.wait() kan alleen worden uitgevoerd door een thread die tijdelijk de 'eigenaar' is van het object synchro. Hier is de reeks:
synchronized(synchro){
…
}// synchronized
die ervoor zorgt dat de thread eigenaar is van het object synchro. Via de bewerking synchro.wait() draagt de thread het eigendom van de synchronisatievergrendeling over. Waarom gebeurt dit? Meestal omdat de thread onvoldoende bronnen heeft om verder te werken. Dus in plaats van de andere threads te blokkeren die wachten op de hulpbron synchro, draagt hij deze af en gaat hij zelf wachten op de hulpbron die hij mist. In ons voorbeeld wacht hij tot de booleaanse waarde peutPasser op ‘waar’ komt te staan. Hoe wordt hij op de hoogte gebracht van deze gebeurtenis? Op de volgende manier:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // als we niet door kunnen, dan wachten we
} catch (Exception e){
…
}
}
peutPasser=false; // geen enkele andere thread mag erdoor
}// synchronized
section critique...
synchronized(synchro){
synchro.notify();
}
Laten we eens kijken naar de eerste thread die de synchronisatievergrendeling passeert. Laten we deze T1 noemen. Stel dat deze thread de booleaanse waarde peutPasser als ‘waar’ aantreft, aangezien hij de eerste is. Hij zet deze waarde dus op ‘onwaar’. Vervolgens verlaat hij de kritieke sectie die vergrendeld is door het object synchro. Een andere thread kan dan de kritieke sectie betreden om de waarde van peutPasser te controleren. Hij zal vaststellen dat deze ‘false’ is en zal vervolgens gaan wachten op een gebeurtenis (wait). Hierdoor draagt hij het eigendom van het object synchro over. Een andere thread kan dan de kritieke sectie betreden: ook deze zal gaan wachten omdat peutPasser onwaar is. Er kunnen dus meerdere threads wachten op een gebeurtenis op het object synchro.
Laten we teruggaan naar de thread T1, die nu aan de beurt is. Deze voert het kritieke gedeelte uit en geeft vervolgens aan dat een andere thread nu aan de beurt is. Dit doet hij met de volgende reeks:
synchronized(synchro){
synchro.notify();
}
Hij moet eerst het object synchro weer in bezit nemen met de instructie synchronized. Dat zou geen probleem moeten zijn, aangezien hij concurreert met threads die, als ze tijdelijk het object synchro verkrijgen, dit moeten vrijgeven via een wait omdat ze peutPasser als ongeldig detecteren. Onze thread T1 zal dus uiteindelijk wel de eigendom van het object synchro verkrijgen. Zodra dit is gebeurd, geeft hij via de bewerking synchro.notify aan dat een van de threads die door een synchro.wait is geblokkeerd, moet worden geactiveerd. Vervolgens geeft hij opnieuw het eigendom van het object synchro vrij, dat vervolgens aan een van de wachtende threads wordt toegewezen. Deze thread zet de uitvoering voort met de instructie die volgt op de wait die hem in de wachtrij had geplaatst. Op zijn beurt voert hij de kritieke sectie uit en voert hij een synchro.notify uit om een andere thread vrij te maken. En zo verder.
Laten we deze werkwijze eens bekijken aan de hand van het eerder besproken telvoorbeeld.
void btnGénérer_actionPerformed(ActionEvent e) {
//threads genereren
// het aantal te genereren threads wordt gelezen
int nbThreads=0;
try{
// het veld met het aantal threads wordt gelezen
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// positief >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//fout
txtStatus.setText("Nombre invalide");
// opnieuw beginnen
txtAGénérer.requestFocus();
return;
}//catch
// RAZ taak-teller
txtGénérés.setText("0"); // taakteller op 0
// eerste thread kan doorgaan
peutPasser=true;
// de threads worden gegenereerd en gestart
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// thread i wordt aangemaakt
tâches[i]=new Thread() {
public void run() {
synchronise();
}
};//thread i
// de naam ervan wordt gedefinieerd
tâches[i].setName(""+i);
// we starten de uitvoering ervan
tâches[i].start();
}//for
}//genereren
Nu voeren de threads niet langer de methode incrémente uit, maar de volgende methode: synchronise:
// stap voor het synchroniseren van de threads
public void synchronise(){
// toegang tot de kritieke sectie aanvragen
synchronized(synchro){
try{
// mag ik doorgaan?
if(! peutPasser){
System.out.println(Thread.currentThread().getName()+ " en attente");
synchro.wait();
}
// we zijn erdoor - andere threads mogen niet door
peutPasser=false;
} catch(Exception e){
txtStatus.setText(""+e);
return;
}//try
}// synchronized
// kritieke sectie
System.out.println(Thread.currentThread().getName()+ " passé");
incrémente();
// we zijn klaar – we maken een eventueel geblokkeerde thread vrij bij de ingang van de kritieke sectie
peutPasser=true;
System.out.println(Thread.currentThread().getName()+ " terminé");
synchronized(synchro){
synchro.notify();
}// synchronized
} // synchroniseert
De methode synchronise is bedoeld om de threads één voor één te verwerken. Hiervoor maakt ze gebruik van een synchronisatievariabele synchro. De methode incrémente wordt nu niet meer beschermd door het sleutelwoord synchronized:
// verhoogt
private void incrémente(){
// we halen het threadnummer op
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// de waarde van de taak-teller wordt gelezen
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// deze wordt verhoogd
compteurs[iThread]++;
// er wordt 100 milliseconden gewacht – de thread verliest dan de processor
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// de nieuwe tellerstand wordt weergegeven
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}// verhogen
Voor 5 threads zijn de verkregen resultaten als volgt:
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é
Stel dat T0 tot en met T4 de 5 threads zijn die door de applicatie zijn gegenereerd. T0 verwerft als eerste de eigendom van de vergrendeling synchro en vindt peutPasser waar. Hij zet peutPasser op ‘false’ en gaat verder: dat is de betekenis van het eerste bericht passé. Naar alle waarschijnlijkheid gaat hij verder en voert hij het kritieke gedeelte uit, met name de methode incrémente. Daarin gaat hij 100 ms in slaapstand (sleep). Hij geeft de processor dus vrij. Deze wordt toegewezen aan een andere thread, de thread T1, die vervolgens de eigendom van het object synchro verkrijgt. Deze thread merkt dat hij niet verder kan en gaat in de wachtrij staan (wait). Vervolgens geeft hij het eigendom van het object synchro en de processor vrij. De processor wordt toegewezen aan de thread T2, die hetzelfde lot ondergaat. Gedurende de 100 ms dat T0 stil ligt, worden de threads T1 tot en met T4 dus in de wacht gezet. Dit is de betekenis van de 4 berichten „in de wacht“. Na 100 ms krijgt T0 de processor weer toegewezen en voltooit het zijn taak: dit is de betekenis van het bericht „0 terminé“. Vervolgens maakt het een van de geblokkeerde threads vrij en wordt het beëindigd. De vrijgekomen processor wordt vervolgens toegewezen aan een beschikbare thread: degene die zojuist is vrijgekomen. In dit geval is dat T1. De thread T1 gaat dan de kritieke sectie binnen: dat is de betekenis van het bericht „1 passé“. Hij doet wat hij moet doen en stopt op zijn beurt 100 ms. De processor is dan beschikbaar voor een andere thread, maar alle threads wachten op een gebeurtenis: geen van hen kan de processor overnemen. Na 100 ms krijgt de thread T1 de processor weer toegewezen en wordt hij beëindigd: dat is de betekenis van het bericht "1 terminé". De threads T1 tot en met T4 zullen zich op dezelfde manier gedragen als T1: dat is de betekenis van de drie reeksen berichten: „voorbij“, „beëindigd“.



