Skip to content

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:

currentThread()
geeft de thread weer die momenteel wordt uitgevoerd
setName()
stelt de naam van de thread in
getName()
naam van de thread
isAlive()
geeft aan of de thread actief is (true) of niet (false)
start()
start de uitvoering van een thread
run()
methode die automatisch wordt uitgevoerd nadat de voorgaande start-methode is uitgevoerd
sleep(n)
stopt de uitvoering van een thread gedurende n milliseconden
join()
blokkerende bewerking – wacht tot de thread is voltooid om door te gaan naar de volgende instructie

De meest gebruikte constructors zijn de volgende:

Thread()
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.
Thread(Runnable object)
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 thread i
            tâches[i].start();

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:

    System.exit(0);

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.

Image

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.

Image

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:

nr.
type
naam
rol
1
JTextField (Bewerkbaar=false)
txtHorloge
geeft de tijd weer
2
JButton
btnGoStop
de klok stoppen of starten

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:

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

De methode run van de thread verwijst naar de methode runHorloge van de applicatie. Zodra dit is gebeurd, wordt de thread gestart:

      // de thread wordt gestart
      thHorloge.start();

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:

  1. de huidige tijd wordt weergegeven in het tekstvak txtHorloge
  2. wacht 1 seconde
  3. 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:

Image

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:

Image

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:

Image

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:
   // instantievariabelen
    Thread[] tâches=null;   // de threads
    int[] compteurs=null;   // de tellers

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:

    private void incremente()

Nu declareren we deze anders:

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

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:

Image

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:

    private synchronized void incrémente()

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