Innholdsoversikt for programutvikling

Multiprosessering

Innledning

Hva er en tråd?

En tråd (engelsk: thread) er en sekvens av instruksjoner innenfor et program.

Multiprosessering har vi dersom flere tråder utføres samtidig. Dette kan oppnås enten ved at to eller flere mikroprosessorer bokstavelig talt utfører hver sin oppgave samtidig, eller ved at én mikroprosessor henter instruksjoner vekselvis fra flere tråder, slik at det synes som om oppgavene utføres samtidig. For oss som programmerere spiller det ingen rolle om det er den ene eller den andre typen multiprosessering som foregår.

Hva kan oppnås ved bruk av tråder?

Enkelte oppgaver i et program er langt mer tidkrevende å utføre enn andre. Tidkrevende oppgaver er for eksempel overføring av data mellom hurtiglageret (memory) og ytre enheter som diverse typer fillagre, skjerm, tastatur eller nettverk. I et tradisjonelt program må all annen programutførelse vente mens slik dataoverføring skjer. Bruker vi multiprosessering (det vil si tråder), kan vi få andre deler av programutførelsen til å fortsette mens for eksempel dataoverføring til og fra ytre enheter pågår. På den måten utnyttes prosessoren mer effektivt og programmet kjører raskere. Nå er det slik at både operativsystemet og profesjonelle programmer, som kjøresystemet for et javaprogram, gjør bruk av tråder "bak kulissene". Det vi skal ta for oss i dette notatet, er hvordan vi kan skrive våre javaprogrammer til å bruke tråder som vi selv definerer.

Hvordan opprette tråder?

Dersom du trenger å få utført en tidkrevende oppgave, bør du altså vurdere å programmere den i form av en egen programtråd. Du kan da gå fram på følgende måte:

  1. Plasser koden for oppgaven i run-metoden til en klasse som implementerer interface Runnable. (Det kan selvsagt skje ved at det i run-metoden står kall på én eller flere metoder som til sammen utfører oppgaven.) interface Runnable er svært enkelt, det inneholder bare én metode:
        public interface Runnable
        {
          void run();
        }
      
    En klasse som implementerer Runnable kan du derfor skrive slik:
        class MinRunnableKlasse implements Runnable
        {
          public void run()
          {
            < kode for oppgaven som tråden skal utføre >
          }
        }
      
  2. Opprett et objekt av klassen du har definert:
          Runnable r = new MinRunnableKlasse();
        
  3. Opprett et Thread-objekt som inneholder ditt Runnable-objekt:
          Thread t = new Thread(r);
        
  4. Start tråden:
          t.start();
        

Merknad

Siden Thread-klassen implementerer interface Runnable og dermed inneholder en run-metode, er det teknisk sett mulig også å definere en tråd ved å definere en subklasse til Thread-klassen og redefinere dens run-metode. Tidligere ble denne framgangsmåten vurdert som likeverdig med den som er beskrevet ovenfor. Men nå blir den ikke lenger anbefalt, fordi den mikser oppgaven som skal utføres med mekanismen for å utføre oppgaven. Det blir nå vurdert som en bedre design å skille oppgaven fra trådmekanismen. Dette har dessuten den fordelen at klassen vår som implementerer Runnable (og definerer oppgaven som skal utføres), i tillegg kan være subklasse til en hvilken som helst annen klasse. Den er ikke bundet til å være subklasse av Thread-klassen.

Legg merke til at vi starter utførelsen av en tråd ved å gjøre kall på dens start-metode. Denne vil opprette nødvendige systemressurser og gjøre kall på run-metoden. NB! Gjør aldri eksplisitt kall på run-metoden!

Eksempel

Klassen NumberThread som er gjengitt nedenfor definerer en tråd. Trådobjektet mottar via konstruktørparametre et tall og et tekstområde. Når tråden kjører, tilføyer den tallet et antall ganger til tekstområdet.

 1 import javax.swing.JTextArea;
 2
 3 /*
 4  * En tråd som lagrer et gitt heltall og tilføyer dette et antall 
 5  * ganger til et gitt tekstområde når tråden kjøres.
 6  */
 7 public class NumberThread implements Runnable
 8 {
 9   private JTextArea output;
10   private int tall;
11   private static final int PRINTANTALL = 1000;
12
13   public NumberThread(int n, JTextArea utskrift)
14   {
15     tall = n;
16     output = utskrift;
17   }
18
19   //Inneholder koden som tråden skal utføre.
20   public void run()
21   {
22     for (int k = 0; k < PRINTANTALL; k++)
23     {
24       output.append(tall + "");
25       if (k % 25 == 0)
26       {
27         output.append("\n");
28       }
29     }
30   }
31 } // NumberThread

Klassen Numbers som er gjengitt nedenfor oppretter og starter fem tråder av typen NumberThread. Alle trådene tilføyer sine tall i det samme tekstområde og dette vises i vinduet som klassen definerer. Når programmet blir kjørt, legger vi merke til følgende: Trådene blir ikke nødvendigvis utført i den rekkefølgen vi har startet dem opp. De blir heller ikke nødvendigvis utført i sin helhet hver for seg. Rekkefølgen vil også endre seg fra gang til gang som vi kjører programmet.

 1 import java.awt.EventQueue;
 2 import javax.swing.*;
 3
 4 /*
 5  * Klassen oppretter etter tur fem tråder av type NumberThread og 
 6  * starter dem. Alle trådene tilføyer sine forskjellige tall i det 
 7  * samme tekstområdet.
 8  */
 9 public class Numbers extends JFrame
10 {
11   private int antTråder = 5;
12
13   public Numbers()
14   {
15     super("Trådeksempel");
16     JTextArea utskrift = new JTextArea(10, 50);
17     getContentPane().add(new JScrollPane(utskrift));
18     setSize(400, 500);
19     setVisible(true);
20     Thread[] tall = new Thread[antTråder];
21     for (int i = 0; i < antTråder; i++)
22     {
23       tall[i] = new Thread(new NumberThread(i + 1, utskrift));
24       tall[i].start();
25     }
26   }
27
28   public static void main(String args[])
29   {
30     EventQueue.invokeLater(new Runnable()
31     {
32
33       public void run()
34       {
35         JFrame vindu = new Numbers();
36         vindu.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
37       }
38     });
39   }
40 }

Merknad

Dersom et program består av flere tråder, er det, dersom trådene ikke på noen måte er synkronisert, umulig å forutsi rekkefølgen for instruksjonene, unntatt innenfor én og samme tråd. Javas swing-komponenter er vanligvis ikke såkalt trådsikre (thread safe). Det betyr at det gjelder spesielle restriksjoner på dem. Vi skal se litt nærmere på dette seinere. I programmet ovenfor brukte trådene JTextArea-klassens append-metode. Dette er en av de få swing-metodene som er spesifisert som trådsikker, slik at den uten risiko kan kalles opp av egenprogrammerte programtråder.

Prioritet

Tråder er tilordnet en heltallig prioritetsverdi i intervallet fra Thread.MIN_PRIORITY (definert som 1 i Thread-klassen) til Thread.MAX_PRIORITY (definert som 10 i Thread-klassen). Thread.NORM_PRIORITY er definert som 5. En tråd vil i utgangspunktet arve prioriteten til tråden som konstruerte den. Du kan sette ønsket prioritet til en verdi i det nevnte intervall ved kall på metoden
setPriority(prioritet).

Normalt vil tråder med høyere prioritet ha forrang framfor tråder av lavere prioritet når det gjelder eksekveringsrekkefølge. Men vær oppmerksom på at trådprioriteter er svært systemavhengige. Den plattformen som kjører programmet kan ha et annet antall prioritetsnivåer enn det Java har. For eksempel har Windows sju prioritetsnivåer. Prioriteten som er satt i et javaprogram vil bli tilordnet en av disse. Kjøresystemet for javaprogrammer i Linux ignorerer prioriteter fullstendig — alle tråder har samme prioritet. Det er få grunner til å sette sine egne prioritetsverdier. Iallfall så bør et program aldri struktureres på den måten at det avhenger av prioritetsnivåer.

Tvinge tråder til å "sove"

Thread-klassen inneholder static-metoden sleep. Med denne kan vi få en tråd til å "sove", det vil si ta en pause i sin utførelse i det spesifiserte tidsintervallet. Metoden finnes i to versjoner: den ene spesifiserer via en long-parameter det antall millisekunder som pausen skal vare. Den andre har i tillegg en int-parameter som kan supplere med et antall nanosekunder i tillegg til millisekundene. Når vi skriver instruksjonen

  try
  {
    Thread.sleep( antMillisekunder );
  }
  catch (InterruptedException e )
  {
    e.printStackTrace();
  }

er det den tråden som er aktiv i øyeblikket som sleep-metoden vil bli brukt på. Ved å bruke denne metoden kan vi medvirke til at andre tråder, særlig dem med lik eller lavere prioritet, får en sjanse til å kjøre. Vær imidlertid oppmerksom på at vi har ingen garanti for at de "sovetidene" som vi spesifiserer for sleep-metoden vil bli nøyaktig overholdt. Vi er nemlig prisgitt de mulighetene som blir tilbudt av det underliggende operativsystemet. Dessuten kan soveperioden bli avbrutt, noe som vi skal se nærmere på seinere.

Oppgave

Legg inn koden ovenfor i for-løkka til NumberThread omtalt ovenfor. Prøv deg med flere forskjellige verdier for antMillisekunder og se på virkningen!

Applikasjonsprogrammer starter opp med én tråd: tråden som utfører main-metoden. Den normale måten som en tråd avsluttes på, er at programkontrollen returnerer fra trådens run-metode. Dersom det i programmet blir opprettet tråder i tillegg til "hovedtråden", bør vi sørge for at disse blir avsluttet før hovedtråden, slik at denne er den siste som blir avsluttet. I programmet Numbers som ble omtalt foran, blir hovedtråden avsluttet når vi lukker JFrame-vinduet.

Eksempel: Bruk av tråder til å forbedre responderingen til et hendelsesbasert program

Dersom et program kjører i én tråd, kan vi for eksempel oppleve at programmet ikke responderer på input mens en løkke blir utført. Det kan se ut som programmet henger. Som et eksempel på dette skal vi ta for oss et program som virker på følgende måte: Når brukeren klikker på en knapp, begynner programmet å tegne tilfeldig plasserte svarte prikker. Etter en tilfeldig tid vil det bli skiftet til rød tegnefarge. Det vi ønsker, er at så snart dette skjer, skal brukeren ved å klikke på en knapp kunne stoppe uttegningen av prikker. Vi vil imidlertid se at dette ikke skjer. Programmet responderer ikke på at brukeren klikker på knappen. Tegneprosessen fortsetter til hele tegne-løkka er ferdig. Eksemplet er hentet fra læreboka Java, Java, Java av Ralph Morelli og Ralph Walde. Klassen Dotty som er gjengitt nedenfor mottar tegnepanelet. Tegnemetoden draw tegner et antall prikker. Etter et tilfeldig antall prikker skifter tegnefargen fra svart til rødt. Metoden clear blanker ut tegneflata og skriver ut hvor mange prikker som er blitt tegnet etter at tegnefargen skiftet til rødt.

 1 /*
 2  * File: Dotty.java
 3  * Author: Java, Java, Java
 4  * Description: This class takes care of the drawing
 5  *  task. When a Dotty is created it is given a number
 6  *  representing the number of dots to draw and a reference
 7  *  to the drawing canvas. When its draw() method is called
 8  *  it begins drawing dots at random locations on the canvas.
 9  *  When its clear() method is called it clears the drawing canvas.
10  */
11
12 import java.awt.*;
13 import javax.swing.*;
14
15 public class Dotty
16 {
17   private static final int HREF = 20, VREF = 20, LEN = 400;//Coordinates
18   private JPanel canvas;
19   private int nDots;          // Number of dots to draw
20   private int nDrawn;         // Number of dots drawn
21   private int firstRed = 0;   // Number of the first red dot
22   private int sleepTime = 1;  // Pause mellom hver tegneoperasjon
23
24   /**
25    * Dotty() constructor is given the number of dots to draw and a
26    * reference to the drawing panel.
27    *
28    * @param dots -- the number of dots @canv -- the reference to
29    * the drawing panel
30    */
31   public Dotty(JPanel canv, int dots)
32   {
33     canvas = canv;
34     nDots = dots;
35   }
36
37   /**
38    * draw() draws ndots dots at random locations. After some random
39    * number of black dots, it changes the drawing color to red.
40    */
41   public void draw()
42   {
43     Graphics g = canvas.getGraphics();
44     for (nDrawn = 0; nDrawn < nDots; nDrawn++)
45     {
46       int x = HREF + (int) (Math.random() * LEN);
47       int y = VREF + (int) (Math.random() * LEN);
48       g.fillOval(x, y, 3, 3);                      // Draw a dot
49
50       if ((Math.random() < 0.001) && (firstRed == 0))
51       {
52         g.setColor(Color.red);                   // Change color to red
53         firstRed = nDrawn;
54       }
55       try
56       {
57         Thread.sleep(sleepTime);
58       }
59       catch (InterruptedException ie)
60       {
61       }
62     } //for
63   } // draw()
64
65   /**
66    * clear() clears the drawing panel
67    */
68   public void clear()
69   {                  // Clear screen and report result
70     Graphics g = canvas.getGraphics();
71     g.setColor(canvas.getBackground());
72     g.fillRect(HREF, VREF, LEN + 3, LEN + 3);
73     g.setColor(Color.black);
74     g.drawString("Dots since first red = " + (nDrawn - firstRed),
75             HREF, VREF + LEN);
76   } // clear()
77 } // Dotty

Vindusklassen RandomDotFrame er gjengitt nedenfor. Den inneholder et panel som kan brukes av klassens Dotty-objekt til å tegne prikker på. De to knappene i vinduet er programmert til å starte og stoppe tegning av prikker. Det vi opplever når vi kjører programmet, er imidlertid at tegneprosessen ikke lar seg stoppe før hele Dotty-klassens for-løkke er gjennomkjørt. Kjøreklasse for programmet finnes i fila Dottytest.java. Bildet nedenfor viser programvinduet etter at programmet er kjørt og tegningen av prikker er forsøkt stoppet ved å klikke på Clear-knappen så snart som det ble tegnet røde prikker.

 1 /*
 2  * File: RandomDotApplet.java
 3  * Author: Java, Java, Java
 4  * Description: This application illustrates the problem
 5  *  of using a single thread to serve both the
 6  *  user interface portion of a program and the
 7  *  computational portion of the program. If the
 8  *  computational portion contains a lengthy loop,
 9  *  the user interface will become very unresponsive.
10
11  *  When the user clicks the Draw button a Dotty
12  *  object begins drawing NDOTS dots at random locations
13  *  on a JPanel. The user is supposed to click on the clear
14  *  button as soon as he or she sees a red dot. When the user
15  *  clicks the Clear button, this is supposed to stop the drawing
16  *  and report the number of red dots that were drawn, thereby 
17  *  measuring the quickness of the user's response.  However, in this
18  *  single-threaded version, the drawing loop cannot be
19  *  interrupted. So the user must wait until all NDOTS
20  *  are drawn before the panel is cleared. Therefore, the program
21  *  cannot provide an accurate measure of the user's response time.
22  */
23
24 import java.awt.*;
25 import javax.swing.*;
26 import java.awt.event.*;
27
28 public class RandomDotFrame extends JFrame implements ActionListener
29 {
30   public final int NDOTS = 10000;
31   private Dotty dotty;                          // The drawing class
32   private JPanel controls = new JPanel();
33   private JPanel canvas = new JPanel();
34   private JButton draw = new JButton("Draw");
35   private JButton clear = new JButton("Clear");
36
37   /**
38    * sets up the user interface which consists of the Draw and
39    * Clear buttons.
40    */
41   public RandomDotFrame()
42   {
43     getContentPane().setLayout(new BorderLayout());
44     draw.addActionListener(this);
45     clear.addActionListener(this);
46     controls.add(draw);
47     controls.add(clear);
48     canvas.setBorder(BorderFactory.createTitledBorder(
49             "Drawing Canvas"));
50     getContentPane().add("North", controls);
51     getContentPane().add("Center", canvas);
52     getContentPane().setSize(400, 400);
53     setSize(450, 500);
54     setVisible(true);
55   }
56
57   /**
58    * actionPerformed() handles the application's action events. However, 
59    * once dotty starts drawing, the clear operation will have to wait
60    * until the drawing is completed. This can lead to a noticeable
61    * delay.
62    */
63   public void actionPerformed(ActionEvent e)
64   {
65     if (e.getSource() == draw)
66     {
67       dotty = new Dotty(canvas, NDOTS);
68       dotty.draw();
69     }
70     else
71     {
72       dotty.clear();
73     }
74   }
75 }

Programmering av egen tegnetråd

Vi skal nå omarbeide tegneprogrammet slik at det responderer øyeblikkelig på brukerinput. Det er da to ting som må gjøres:

Definere egen tegnetråd: Vi lar klassen Dotty implementere interface Runnable. Det innebærer at vi i klassen må implementere en run-metode. Vi lar denne kort og godt gjøre kall på draw-metoden som gjennomfører tegneløkka. I denne er det nå helt avgjørende at det er lagt inn en liten pause for hver runde:

72       try
73       {
74         Thread.sleep(1);                      // Sleep for an instant
75       }
76       catch (InterruptedException e)
77       {
78         System.out.println(e.getMessage());
79       }

Denne pausen finnes også i den gamle versjonen. Men der ble den bare lagt inn for å sinke gjennomkjøringen av for-løkka, slik at brukeren kunne rekke å reagere. Nå gir den i tillegg andre tråder, det vil her si tråden for hendelseshåndtering, sjanse til å eksekvere, slik at denne kan reagere på brukerinput. Den omarbeidete klassen, RunnableDotty, er gjengitt nedenfor.

 1 /*
 2  * File: RunnableDotty.java
 3  * Author: Java, Java, Java
 4  * Description: This class takes care of the drawing
 5  *  task. When a Dotty is created it is given a number
 6  *  representing the number of dots to draw and a reference
 7  *  to the drawing canvas. When its draw() method is called
 8  *  it begins drawing dots at random locations on the canvas.
 9  *  When its clear() method is called it clears the drawing canvas.
10
11  * This version of Dotty implements the Runnable interface,
12  *  enabling it to run as a separater thread from the applet
13  *  that creates it. This design leads to a more responsive
14  *  user interface.
15  */
16 import java.awt.*;
17 import javax.swing.*;
18
19 public class RunnableDotty implements Runnable
20 {
21   private static final int HREF = 20, VREF = 20, LEN = 400;//Coordinates
22   private JPanel canvas;
23   private int nDots;                 // Number of dots to draw
24   private int nDrawn;                // Number of dots drawn
25   private int firstRed = 0;          // Number of the first red dot
26   private boolean isCleared = false; // The panel has been cleared
27
28   public void run()
29   {
30     draw();
31   }
32
33   /**
34    * Dotty() constructor is given the number of dots to draw and
35    * a reference to the drawing panel.
36    *
37    * @param dots -- the number of dots @canv -- the reference to
38    * the drawing panel
39    */
40   public RunnableDotty(JPanel canv, int dots)
41   {
42     canvas = canv;
43     nDots = dots;
44   }
45
46   /**
47    * draw() draws ndots dots at random locations. After some random
48    * number of black dots, it changes the drawing color to red. Note
49    * that this version of draw() sleeps for an instance on each each
50    * iteration. This gives the applet thread a chance to run, making
51    * it more responsive to user input.
52    */
53   public void draw()
54   {
55     Graphics g = canvas.getGraphics();
56     if (!SwingUtilities.isEventDispatchThread())
57     {
58       System.out.println("RunnableDotty");
59     }
60     for (nDrawn = 0; !isCleared && nDrawn < nDots; nDrawn++)
61     {
62       int x = HREF + (int) (Math.random() * LEN);
63       int y = VREF + (int) (Math.random() * LEN);
64       g.fillOval(x, y, 3, 3);                      // Draw a dot
65
66       if (Math.random() < 0.001 && firstRed == 0)
67       {
68         g.setColor(Color.red);                  // Change color to red
69         firstRed = nDrawn;
70       }
71
72       try
73       {
74         Thread.sleep(1);                      // Sleep for an instant
75       }
76       catch (InterruptedException e)
77       {
78         System.out.println(e.getMessage());
79       }
80     } //for
81   } // draw()
82
83   /**
84    * clear() clears the drawing panel
85    */
86   public void clear()
87   {
88     isCleared = true;
89     Graphics g = canvas.getGraphics();
90     g.setColor(canvas.getBackground());
91     g.fillRect(HREF, VREF, LEN + 3, LEN + 3);
92     g.setColor(Color.black);
93     g.drawString("Dots since first red = "
94             + (nDrawn - firstRed), HREF, VREF + LEN);
95   } // clear()
96 }

Den modifiserte vindusklassen ThreadedRandomDotFrame er gjengitt nedenfor. Klassen tjener også som lytteobjekt. Tegnetråden blir opprettet og startet av lytteobjektet:

      if (e.getSource() == draw)
      {
          dotty = new RunnableDotty( canvas, NDOTS );
          dottyThread = new Thread(dotty);
          dottyThread.start();
      }

Kjøreklasse for programmet finnes i fila RunnableDottyTest.java. Bildet nedenfor viser programvinduet etter at det er foretatt en kjøring og tegning av prikker ble stoppet etter at tegnefargen ble skiftet til rødt. Vi ser at det ble tegnet ut 58 røde prikker før brukeren rakk å stoppe uttegningen.

 1 /*
 2  * File: ThreadedRandomDotFrame.java
 3  * Author: Java, Java, Java
 4  * Description: This application creates a multithreaded
 5  *  solution to the problem of drawing random dots.
 6  *  In this case the user interface portion of a program and the
 7  *  computational portion of the program are separate threads.
 8  *  By making the drawing thread sleep on each iteration, the
 9  *  applet thread can respond to user input, thereby making
10  *  the program more responsive.
11
12  *  When the user clicks the Draw button a Dotty
13  *  object begins drawing NDOTS dots at random locations
14  *  on a JPanel. The user is supposed to click on the clear
15  *  button as soon as he or she sees a red dot. When the user
16  *  clicks the Clear button, this is supposed to stop the drawing
17  *  and report the number of red dots that were drawn, thereby measuring
18  *  the quickness of the user's response.  In this multithreaded
19  *  design, the program does indeed stop drawing as soon as the
20  *  user clicks the button.
21  */
22
23 import java.awt.*;
24 import javax.swing.*;
25 import java.awt.event.*;
26
27 public class ThreadedRandomDotFrame extends JFrame
28         implements ActionListener
29 {
30   public final int NDOTS = 10000;
31   private RunnableDotty dotty;              // The drawing class
32   private Thread dottyThread;
33   private JPanel controls = new JPanel();
34   private JPanel canvas = new JPanel();
35   private JButton draw = new JButton("Draw");
36   private JButton clear = new JButton("Clear");
37
38   /**
39    * sets up the user interface which consists of the Draw and Clear
40    * buttons.
41    */
42   public ThreadedRandomDotFrame()
43   {
44     getContentPane().setLayout(new BorderLayout());
45     draw.addActionListener(this);
46     clear.addActionListener(this);
47     controls.add(draw);
48     controls.add(clear);
49     canvas.setBorder(
50             BorderFactory.createTitledBorder("Drawing Canvas"));
51     getContentPane().add("North", controls);
52     getContentPane().add("Center", canvas);
53     getContentPane().setSize(400, 400);
54     setSize(450, 500);
55     setVisible(true);
56   } // init()
57
58   /**
59    * actionPerformed() handles the application's action events. Note
60    * that dotty, the drawing object, is created as a Thread and then
61    * started. Since dotty is a separate thread of execution, it can be
62    * made to sleep occasionally to give the event dispatch thread a
63    * chance to run. This makes the application more responsive to user
64    * input.
65    */
66   public void actionPerformed(ActionEvent e)
67   {
68     if (e.getSource() == draw)
69     {
70       if (SwingUtilities.isEventDispatchThread())
71       {
72         System.out.println("EventDispatchThread");
73       }
74       dotty = new RunnableDotty(canvas, NDOTS);
75       dottyThread = new Thread(dotty);
76       dottyThread.start();
77     }
78     else
79     {
80       dotty.clear();
81     }
82   } // actionPerformed()
83 }

Stopp av tråder

Som tidligere nevnt, bør vi sørge for at de trådene vi selv definerer og oppretter blir avsluttet før programmets hovedtråd, slik at denne er den siste som avsluttes. Den beste måten å gjøre dette på er å bruke en logisk kontrollvariabel som virker slik at tråden hopper ut av sin run-metode når den logiske kontrollvariabelen settes til true eller false. I tråden RunnableDotty ovenfor ble det til dette bruk opprettet variabelen

  private boolean isCleared = false;

Verdien til denne variabelen blir det testet på i betingelsen til for-løkka i tegnemetoden:

    for (nDrawn = 0; !isCleared && nDrawn < nDots; nDrawn++)

Når brukeren klikker på knappen som skal stoppe tegneprosessen, er det kall på RunnableDotty-objektets clear-metode. Den setter isCleared til true. Dermed blir draw-metoden avsluttet. Da blir også tegnetrådens run-metode avsluttet, siden kallet på draw-metoden er eneste instruksjon i denne.

Merknad

Opprinnelig så fantes det i Thread-klassen en stop-metode for å tvinge tråder til å avslutte. Denne metoden er nå markert som foreldet. Det blir anbefalt at det aldri gjøres kall på den. Grunnen er at den har usikker virkning og kan få uforutsigbare konsekvenser. Dette er forklart nærmere i notatet Why is Thread.stop deprecated?.

Tråd-tilstander

En tråd kan være i en av følgende seks tilstander:

Hver av tilstandene blir kort forklart i det følgende.

Nye tråder

Like etter at du har opprettet en tråd med new-operatoren brukt på Thread-klassen, kjører den foreløpig ikke. Koden den inneholder blir foreløpig ikke utført. Da er tråden i ny-tilstanden. Før en tråd kan kjøre, må operativsystemet utføre diverse "bokholderi".

Kjørbare tråder

Så snart du har gjort kall på trådens start-metode, er den i kjørbar tilstand. Men vi kan ikke vite om en kjørbar tråd virkelig kjører! Det er opp til operativsystemet å gi tråden anledning til å kjøre. Og så snart en tråd kjører, vil den nødvendigvis ikke fortsette å gjøre det kontinuerlig inntil den er ferdig med sin oppgave. Det er faktisk ønskelig at kjørende tråder fra tid til annen tar en pause, slik at andre tråder kan få slippe til. Detaljene i hvordan dette virker er bestemt av operativsystemet. Vanlig opplegg nå til dags er at hver tråd blir tildelt et bestemt tidsintervall der den kan kjøre. Når dette intervallet er utløpt, må den vente mens en eller flere andre tråder får anledning til å kjøre i et tilsvarende tidsintervall, før tråden igjen får slippe til i et nytt tidsintervall. Slik fortsetter det inntil alle trådene er ferdige med sin oppgave.

Blokkerte og ventende tråder

En tråd som er blokkert eller ventende, har midlertidig avbrutt sin kjøring. Det er opp til operativsystemet å aktivere den igjen. Detaljene for dette er avhengig av hvorfor tråden er kommet i sin inaktive tilstand. Noe av det vil bli klartgjort via eksempler som vi skal ta for oss seinere. Tidsinnstilt venting får vi blant annet som følge av den sleep-metoden som vi allerede har brukt i eksemplene foran.

Avsluttede tråder

En tråd vil bli avsluttet som følge av en av følgende to årsaker:

Samarbeid mellom tråder - synkronisering

Når flere tråder skal gjøre bruk av de samme data, må vi være ekstra påpasselige med at kommunikasjonen mellom trådene foregår på riktig måte og at instruksjoner utføres i riktig rekkefølge. Et typisk eksempel på dette er et såkalt produsent-konsument-forhold mellom trådene: Den ene tråden produserer verdier som den andre tråden skal benytte, og verdiene skal benyttes i samme rekkefølge som de blir produsert.

Som eksempel på et slikt produsent-konsument-forhold skal vi ta for oss simulering av et bakeriutsalg der kundene trekker kølapper etter hvert som de kommer. De skal etter tur bli betjent av en ekspeditør i samme rekkefølge som de trakk kønumrene. (Eksemplet er i utgangspunktet hentet fra læreboka Java, Java, Java av Ralph Morelli og Ralph Walde, men noe omarbeidet og utvidet.)

Programmet er bygget opp på følgende måte: Klassen TakeANumber simulerer apparatet som tildeler nye kønumre og viser neste kønummer som skal ekspederes. Klassen er gjengitt nedenfor.

 1 /*
 2  * File: TakeANumber.java
 3  * Author: Java, Java, Java
 4  * Description: An instance of this class serves as
 5  *  a shared resource for the customers and clerk threads
 6  *  of the bakery simulation. This object contains two
 7  *  instance variables, both of which are initialized to 0.
 8  *  The variable next represents the next place in line.
 9  *  This simulates the "ticket" given to customers as they
10  *  arrive. The variable "serving" represents the next
11  *  customer to be served. This simulates the "now serving"
12  *  display that the clerk accesses to determine who's next.
13
14  * The problem with this version of the simulation is that
15  *  the clerk is given number regardless of whether there
16  *  are customers waiting or not.
17  */
18
19 class TakeANumber
20 {
21   private int next = 0;     // Next place in line
22   private int serving = 0;  // Next customer to serve
23
24   /**
25    * nextNumber() is called by each customer as it enters the bakery.
26    * It returns the customer's "ticket". The first customer will be
27    * get 1. The fact that this method is synchronized means that it
28    * cannot be interrupted by another customer, once it starts
29    * executing. This guarantees that customer's have mutually exclusive
30    * access to the "tickets".
31    *
32    * @return an int representing a customer's waiting number
33    */
34   public synchronized int nextNumber()
35   {
36     next = next + 1;
37     return next;
38   } // nextNumber()
39
40   /**
41    * nextCustomer() is called by the clerk to decide who should
42    * be served next.
43    *
44    * @return an int representing a customer to be served next
45    */
46   public int nextCustomer()
47   {
48     ++serving;
49     return serving;
50   } // nextCustomer()
51 } // TakeANumber

Klassen Customer simulerer en kunde. Den er definert som en egen tråd. Tråden trekker et kønummer. Klassen er gjengitt nedenfor.

 1 /*
 2  * File: Customer.java
 3  * Author: Java, Java, Java
 4  * Description: This class defines a customer thread for
 5  *  the bakery simulation. Each customer object has a
 6  *  reference to the TakeANumber gadget. When a customer
 7  *  arrives in the bakery, it takes a number and then
 8  *  waits to be served.
 9  */
10
11 import javax.swing.JTextArea;
12
13 public class Customer implements Runnable
14 {
15   private static int number = 10000;      // Initial ID number
16   private int id;
17   private TakeANumber takeANumber;
18   private JTextArea utskrift;
19
20   /**
21    * Customer() constructor gives each customer a reference to the
22    * shared TakeANumber gadget and gives each an id number.
23    */
24   public Customer(TakeANumber gadget, JTextArea t)
25   {
26     id = ++number;
27     takeANumber = gadget;
28     utskrift = t;
29   }
30
31   /**
32    * run() is the main algorithm for the customer thread. It just
33    * takes a number when it enters the bakery and then waits to be
34    * served until the clerk calls its number.
35    */
36   public void run()
37   {
38     try
39     {
40       Thread.sleep((int) (Math.random() * 1000));
41       utskrift.append("\nCustomer " + id + " takes ticket "
42               + takeANumber.nextNumber());
43     }
44     catch (InterruptedException e)
45     {
46       System.out.println("Exception " + e.getMessage());
47     }
48   } // run()
49 } // Customer

Klassen Clerk simulerer ekspeditøren. Denne er også definert som en egen tråd. Den henter neste nummer i køen for ekspedering inntil en maksimaltid blir overskredet. Klassen er gjengitt nedenfor.

 1 /*
 2  * File: Clerk.java
 3  * Author: Java, Java, Java
 4  * Description: This class defines the clerk thread of
 5  *  the bakery simulation. The clerk has a reference to
 6  *  the TakeANumber gadget. The clerk "serves" the next
 7  *  customer by repeatedly calling takeANumber.nextCustomer().
 8  *  To simulate the randomness involved in serving times,
 9  *  the clerk sleeps for a random interval on each iteration of
10  *  its run loop.
11  */
12
13 import javax.swing.JTextArea;
14
15 public class Clerk implements Runnable
16 {
17   private TakeANumber takeANumber;
18   private int åpningstid = 5000; // ant. millisekunder kjøretid
19   private int tid = 0; // ant. millisekunder kjørt
20   private JTextArea utskrift;
21
22   /**
23    * Clerk() constructor gives the clear a reference to the
24    * TakeANumber gadget.
25    */
26   public Clerk(TakeANumber gadget, JTextArea t)
27   {
28     takeANumber = gadget;
29     utskrift = t;
30   }
31
32   /**
33    * run() is the main algorithm for the clerk thread. Note that
34    * it runs in an infinite loop. The clerk thread should block if
35    * there are no customers to serve.
36    */
37   public void run()
38   {
39     while (tid < åpningstid)
40     {
41       try
42       {
43         int ekspederingstid = (int) (Math.random() * 100);
44         tid += ekspederingstid;
45         Thread.sleep(ekspederingstid);
46         utskrift.append("\nClerk serving ticket "
47                 + takeANumber.nextCustomer());
48       }
49       catch (InterruptedException e)
50       {
51         System.out.println("Exception " + e.getMessage());
52       }
53     } //while
54   } //run()
55 } // Clerk

Klassen Bakery er selve applikasjonsklassen med main-metode. Klassen definerer og oppretter et vindu for utskrift. Den oppretter et TakeANumber-objekt, en Clerk-tråd, fem Customer-tråder, og starter alle trådene. Alle trådene kommuniserer med TakeANumber-objektet.

 1 /*
 2  * File: Bakery.java
 3  * Author: Java, Java, Java
 4  * Description: This is the main class for the bakery
 5  *  simulation. It first creates a TakeANumber gadget, which
 6  *  simulates the gadget that keeps track of waiting lines in
 7  *  banks and bakeries. It then creates a clerk and 5 customers,
 8  *  each implemented as a separate thread, and each of which is
 9  *  passed a reference to the TakeANumber object. Thus all the
10  *  threads in the simulation share the TakeANumber resource.
11  *  Each thread is started. Controls built into the
12  *  threads themselves are responsible for managing the simulation.
13  */
14
15 import java.awt.EventQueue;
16 import javax.swing.*;
17
18 public class Bakery extends JFrame
19 {
20   public Bakery()
21   {
22     super("Kølapper");
23     JTextArea utskrift = new JTextArea(20, 20);
24     utskrift.setEditable(false);
25     utskrift.setText("Starting clerk and customer threads");
26     getContentPane().add(new JScrollPane(utskrift));
27     TakeANumber numberGadget = new TakeANumber();
28     Thread clerk = new Thread(new Clerk(numberGadget, utskrift));
29     clerk.start();
30     for (int k = 0; k < 5; k++)
31     {
32       Thread customer = new Thread(
33               new Customer(numberGadget, utskrift));
34       customer.start();
35     }
36     setSize(300, 300);
37     setVisible(true);
38   }
39
40   public static void main(String args[])
41   {
42     EventQueue.invokeLater(new Runnable()
43     {
44       public void run()
45       {
46         JFrame vindu = new Bakery();
47         vindu.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
48       }
49     });
50   } // main()
51 } // Bakery

Bildet nedenfor viser en del av utskriften fra en kjøring av programmet.

Av utskriften fra programmet ser vi at kundene blir betjent i gal rekkefølge i forhold til kønumrene: Kønumre blir dels betjent før de er trukket og det blir betjent kønumre som ikke eksisterer. Vi skal se nærmere på hva dette skyldes og hvilke muligheter vi har til å synkronisere tråder, slik at instruksjoner blir utført i riktig rekkefølge.

Synkronisering

I programmet ovenfor merker vi oss at TakeANumber-klassens metode nextNumber er synkronisert:

34   public synchronized int nextNumber()
35   {
36     next = next + 1;
37     return next;
38   } // nextNumber()

Metoden returnerer neste kønummer. For at dette skal virke riktig, er det viktig at bare én kundetråd om gangen kan gjøre kall på den, slik at ikke flere kunder får tildelt samme kønummer. Når en synkronisert metode blir kalt opp, blir det satt en lås på objektet, slik at ingen andre tråder kan gjøre kall på metoden, og heller ikke på eventuelle andre synkroniserte metoder i samme objektet. Metoder som ikke er synkroniserte, kan kalles opp.

I både Customer- og Clerk-trådene er det lagt inn pauser (ved bruk av sleep) slik at andre tråder er sikret mulighet til å kjøre.

Feilen med simuleringen vår er altså at ekspeditøren ekspederer ikke-eksisterende kunder. For å rette på dette, må vi sørge for at ekspeditøren sjekker om det er kunder i køen før neste kunde blir ekspedert. I de neste eksemplene skal vi se nærmere på hvordan dette kan gjøres og hva det må tas hensyn til for å få en sikker løsning.

Versjon 2 av bakeri-simuleringen

Vi prøver å reparere programmet ved at vi i klassen TakeANumber som simulerer kønummerautomaten legger inn metoden

  public boolean customerWaiting()
  {
    return next > serving;
  }

Denne metoden vil returnere true dersom det er et kønummer som ikke er ekspedert. Ellers returnerer den false. Den modifiserte klassen finnes i fila TakeANumber2.java.

Clerk-klassen må vi modifisere ved at vi nå bruker kønummerautomat av type TakeANumber2. Dessuten må vi sørge for å gjøre bruk av den nye metoden som ble lagt inn i kønummerautomaten: Neste kønummer må bli ekspedert bare dersom det er en ubetjent kunde i køen, det vil si bare dersom customerWaiting returnerer true. Dette er lagt inn i den modifiserte run-metoden til Clerk-tråden:

38   public void run()
39   {
40     while (tid < åpningstid)
41     {
42       try
43       {
44         int ekspederingstid = (int) (Math.random() * 100);
45         tid += ekspederingstid;
46         Thread.sleep(ekspederingstid);
47
48         if (takeANumber.customerWaiting())
49         {
50           utskrift.append("\nClerk serving ticket "
51                   + takeANumber.nextCustomer());
52         }
53       }
54       catch (InterruptedException e)
55       {
56         System.out.println("Exception " + e.getMessage());
57       }
58     } // while
59   } // run()

Den modifiserte Clerk-klassen finnes i fila Clerk2.java.

Eneste endringen som er nødvendig i kundetråden, er å bruke et objekt av den modifiserte typen kønummerautomat. Modifisert tråd finnes i fila Customer2.java.

I applikasjonsklassen Bakery må vi nå bruke en kønummerautomat av den nye typen, og vi må bruke ekspeditør- og kundetråder av de nye typene. De nødvendige modifikasjonene er lagt inn i fila Bakery2.java. Bildet nedenfor viser resultatet av en kjøring. Programmet ser nå ut til å virke riktig.

Problem: mulig avbrudd av tråder

Versjon 3 av bakerisimuleringen

Det er umulig å forutsi når en tråd kan bli avbrutt eller må vike for en annen tråd. Dersom det er viktig at en kodebolk blir utført uten avbrudd, må vi sørge for å programmere slik at dette blir sikret. For å illustrere problemet, skal vi ta for oss følgende instruksjon fra Customer2-trådens run-metode:

41       utskrift.append("\nCustomer " + id + " takes ticket "
42               + takeANumber.nextNumber());

Instruksjonen er satt sammen av flere enkeltinstruksjoner. Det kan derfor godt tenkes at Customer2-tråden blir avbrutt mellom retur av neste nummer fra TakeANumber2 og utskrift av dette på tekstområdet. For å framprovosere dette, skal vi legge inn en liten pause mellom de to nevnte operasjonene, slik det er gjort i Customer3-klassen til versjon 3 av simuleringsprogrammet. Kode med den innlagte pause ser ut som følger:

47       int myturn = takeANumber.nextNumber();
48       Thread.sleep((int) (Math.random() * 1000));
49       utskrift.append("\nCustomer " + id + " takes ticket " + myturn);

Fila Customer3.java inneholder den nevnte modifikasjonen. Denne brukes i applikasjonsklassen Bakery3. Følgende bilde viser resultatet av en kjøring.

Det kan her se ut som kundene blir ekspedert før de har tatt kønummer. Feilen er imidlertid at rekkefølgen for rapportering (utskrift) er gal: Det blir rapportert ekspedering av et kønummer før rapportering av at kønummeret er blitt trukket. Vi må derfor endre programmet slik at vi sikrer oss at det ikke blir noe avbrudd mellom trekking av neste nummer og rapportering av dette.

Versjon 4 av bakerisimuleringen

Nøkkelen til korrigering av programmet, slik at det er garantert å virke riktig, er å fokusere på den ressursen som deles av flere tråder: Det er kønummerautomaten. Som nevnt tidligere, vil en synkronisert metode garantere at bare én tråd om gangen kan kalle opp metoden og at eksekveringen av metoden ikke blir avbrutt. Vi endrer derfor programmet slik at utskriftsinstruksjonene flyttes fra kunde- og ekspeditørobjektene til metodene nextNumber og nextCustomer i TakeANumber2-klassen. Vi synkroniserer dessuten alle metodene i denne klassen, slik at bare én tråd om gangen kan bruke kønummerautomaten. Den modifiserte klassen TakeANumber4 er gjengitt nedenfor.

 1 /*
 2  * File: TakeANumber4.java
 3  * Author: Java, Java, Java
 4  * Description: An instance of this class serves as
 5  *  a shared resource for the customers and clerk threads
 6  *  of the bakery simulation. This object contains two
 7  *  instance variables, both of which are initialized to 0.
 8  *  The variable next represents the next place in line.
 9  *  This simulates the "ticket" given to customers as they
10  *  arrive. The variable "serving" represents the next
11  *  customer to be served. This simulates the "now serving"
12  *  display that the clerk accesses to determine who's next.
13
14  * In this version all of its methods are synchronized and
15  *  the println() statements that report the state of the
16  *  simulation have been moved into these syncrhonized methods.
17  *  In effect, each of these operations is now a critical
18  *  section that cannot be adversely affected by a system interruption
19  *  during the middle of its execution.
20  */
21
22 import javax.swing.JTextArea;
23
24 public class TakeANumber4
25 {
26   private int next = 0;     // Next place in line
27   private int serving = 0;  // Next customer to serve
28   private JTextArea utskrift;
29
30   public TakeANumber4(JTextArea t)
31   {
32     utskrift = t;
33   }
34
35   /**
36    * nextNumber() is called by each customer as it enters the bakery.
37    * It returns the customer's "ticket". The first customer will be
38    * get 1. The fact that this method is synchronized means that it
39    * cannot be interrupted by another customer, once it starts
40    * executing. This guarantees that customer's have mutually exclusive
41    * access to the "tickets".
42    *
43    * @return an int representing a customer's waiting number
44    */
45   public synchronized int nextNumber(int custId)
46   {
47     next = next + 1;
48     utskrift.append("\nCustomer " + custId + " takes ticket " + next);
49     return next;
50   }
51
52   /**
53    * nextCustomer() is called by the clerk to decide who should be
54    * served next.
55    *
56    * @return an int representing a customer to be served next
57    */
58   public synchronized int nextCustomer()
59   {
60     ++serving;
61     utskrift.append("\nClerk serving ticket " + serving);
62     return serving;
63   }
64
65   /**
66    * customerWaiting() returns true iff there is a customer to be served
67    *
68    * @return a boolean representing whether or not a customer is waiting
69    */
70   public synchronized boolean customerWaiting()
71   {
72     return next > serving;
73   }
74 } // TakeANumber4

Modifisert ekspeditørklasse med fjernet utskriftsinstruksjon finnes i fila Clerk4.java. Klassen bruker en kønummerautomat av den nye typen TakeANumber4.

Modifisert kundeklasse med fjernet utskriftsinstruksjon finnes i fila Customer4.java. Også denne klassen bruker en kønummerautomat av den nye typen TakeANumber4.

Fila Bakery4.java inneholder modifisert applikasjonsklasse. Den bruker de modifiserte klassene for denne fjerde versjonen av programmet. Bildet nedenfor viser utskrift av en kjøring. Av dette ser vi at det nå igjen er blitt riktig rekkefølge.

Bruk av wait og notify for å koordinere tråder

Vår siste versjon av bakerisimuleringen virker riktig, men den har likevel en svakhet: Mens ekspeditøren venter på neste kunde, blir det kontinuerlig sjekket om det er noen kunde som venter. Dette legger beslag på prosessoren og hindrer andre tråder i å kjøre. En mye bedre løsning ville være å tvinge ekspeditørtråden til å vente inntil det dukket opp en kunde, og varsle ekspeditørtråden når det skjer. Denne virkemåten kan vi få til ved å bruke javas wait/notify-mekanisme.

Metodene wait, notify og notifyAll er definert i Object-klassen og blir derfor arvet til alle klasser. De kan bare brukes innenfor synkroniserte metoder og har følgende virkning:

wait():
setter aktiv tråd til en ventetilstand og plasserer den i en kø av ventende tråder.
notify():
henter første tråd i ventekøen ut av denne og plasserer den i ready-køen.
notifyAll():
henter alle tråder ut av ventekøen og plasserer dem i ready-køen.

Det er av avgjørende betydning at en eller annen tråd som ikke er i ventekøen gjør kall på notify eller notifyAll periodisk. En tråd som det er brukt wait på har nemlig ingen mulighet til å frigjøre seg selv fra ventetilstanden. Dens skjebne er overlatt til andre tråder. Dersom ingen av dem frigjør tråden som er i ventetilstanden, vil den aldri komme ut av den og kjøre igjen. Dette kan resultere i såkalte vranglåssituasjoner (engelsk: dead lock): alle trådene er i ventesituasjon og venter på at en annen tråd skal frigjøre dem. Dersom alle andre tråder er blokkerte og den siste aktive tråden kaller wait uten å frigjøre noen av de andre, vil alle trådene bli blokkert og programmet henger.

I praksis kan det være risikabelt å gjøre kall på notify fordi vi ikke har noen kontroll på hvilken tråd som da blir frigjort. Vi kan risikere at det blir frigjort en tråd som ikke har mulighet til å kjøre videre. Det blir derfor anbefalt å bruke notifyAll istedenfor notify, slik at alle blokkerte tråder blir frigjort.

Når bør vi så bruke notifyAll? Det bør vi alltid gjøre når tilstanden til et objekt kan ha endret seg slik at det kan være til fordel for andre tråder.

Sjekk at alle kall på wait er matchet med et tilsvarende kall på notifyAll!

Obs! Vanligvis bør et kall på wait befinne seg innenfor en løkke på formen

    while ( !<ok å fortsette> )
      wait();

Versjon 5 av bakerisimuleringen

Vi skal nå bruke wait/notify-mekanismen i bakerisimuleringen. Dersom ekspeditøren gjør kall på nextCustomer uten at det er noen kunde som venter, skal ekspeditøren settes til å vente. Dette får vi til ved å modifisere nextCustomer-metoden til kønummerautomaten på følgende måte:

41   public synchronized int nextCustomer()
42   {
43     try
44     {
45       while (next <= serving)
46       {
47         utskrift.append("\nClerk waiting ");
48         wait(); //aktiv tråd settes til å vente
49       }
50       ++serving;
51       utskrift.append("\nClerk serving ticket " + serving);
52       return serving;
53     }
54     catch (InterruptedException e)
55     {
56       System.out.println("Exception " + e.getMessage());
57       return -1;
58     }
59   } // nextCustomer()

Husk at det er ekspeditøren som gjør kall på nextCustomer. Det er derfor ekspeditørtråden som utfører instruksjonene i metoden og det vil da også bli den som blir satt til å vente når det ikke er noen kunder som venter på å bli ekspedert. Når ekspeditør-tråden er blitt satt til å vente, og den så starter opp igjen, så bør den sjekke om det er noen kunder som venter. Derfor brukes en while-løkke i metoden nextCustomer.

Når det kommer en ny kunde og trekker et kønummer, må vi sørge for å varsle ekspeditøren om at nå er det en kunde som venter på tur. Dette får vi til ved at vi i metoden nextNumber legger inn en instruksjon som varsler alle ventende tråder:

33   public synchronized int nextNumber(int custId)
34   {
35     next = next + 1;
36     utskrift.append("\nCustomer " + custId + " takes ticket " + next);
37     notifyAll();
38     return next;
39   } // nextNumber()

Metoden customerWaiting som vi hadde i tidligere versjoner av kønummerautomaten, er nå unødvendig: Vi bruker wait/notify-mekanismen isteden. Metoden kan derfor fjernes fra klassen. Fila TakeANumber5.java inneholder oppdatert klasse med de nevnte endringene.

Endringene som må gjøres i tråden som definerer ekspeditøren, er at det brukes en kønummerautomat av den nye typen. Dessuten kan vi fjerne kallet på metoden customerWaiting, slik at run-metoden kan forenkles til følgende utseende:

32   public void run()
33   {
34     while (true)
35     {
36       try
37       {
38         Thread.sleep((int) (Math.random() * 1000));
39         takeANumber.nextCustomer();
40       }
41       catch (InterruptedException e)
42       {
43         System.out.println("Exception: " + e.getMessage());
44       }
45     } // while
46   } // run()

Oppdatert klasse finnes i fila Clerk5.java.

Eneste endringen som kreves i kundeklassen, er å bruke kønummerautomat av den nye typen. Dette er gjort i fila Customer5.java.

I applikasjonsklassen må vi bruke kønummerautomat, ekspeditør, og kunder av de nye typene. Dette er gjort i fila Bakery5.java. Bildet nedenfor viser et kjøreresultat med det nye programmet.

Tråder og swing-komponenter

Hovedregel: Så snart en swing-komponent er realisert (det vil si vist på skjermen), bør den ikke endres av andre tråder enn tråden for hendelseshåndtering (event-dispatching-thread).

Unntak: Noen metoder er kvalifisert som trådsikre. De viktigste av disse er metoden setText for tekstfelter og tekstområder, samt append for tekstområder.

Metoder som er trådsikre er spesifisert til å være det i API-dokumentasjonen.

Dersom vi ønsker oppdatering av swing-komponenter initiert fra andre tråder enn tråden for hendelseshåndtering, kan vi gjøre bruk av metoden

  SwingUtilities.invokeLater( <Runnable-objekt> );

Ønsket kode for oppdatering av swing-komponenter skal være inneholdt i run-metoden til objektet som er aktuell parameter i kallet på invokeLater. Metoden returnerer øyeblikkelig. Koden som er inneholdt i den tilhørende run-metoden vil bli utført av tråden for hendelseshåndtering på et seinere tidspunkt.

Eksempel: versjon 6 av bakerisimuleringen

Som eksempel på hvordan invokeLater kan brukes, skal vi omarbeide den siste versjon av programmet for bakerisimulering slik at den bruker invokeLater når utskriften i vinduet skal oppdateres. (Det skal her være unødvendig å gjøre det, siden append er påstått å være trådsikker.)

Klassen Tekstoppdaterer som er gjengitt nedenfor implementerer interface Runnable. Via konstruktørparameter tar den imot det tekstområde som skal oppdateres og den tekst som skal tilføyes i tekstområdet. Dette gjøres av run-metoden ved kall på append.

 1 import javax.swing.*;
 2
 3 //Oppdaterer et tekstområde på en trådsikker måte.
 4 public class Tekstoppdaterer implements Runnable
 5 {
 6   private JTextArea tekstområde;
 7   private String tekst;
 8
 9   public Tekstoppdaterer( JTextArea a, String s )
10   {
11     tekstområde = a;
12     tekst = s;
13   }
14
15   public void run()
16   {
17     tekstområde.append( tekst );
18   }
19 }

I kønummerautomaten, som skal foreta oppdatering av tekstområdet, må utskriftsinstruksjonene erstattes av kall på SwingUtilities.invokeLater. Som parameter må det brukes et Tekstoppdaterer-objekt, med tekstområdet og ønsket tekst som konstruktørparametre. Den modifiserte klassen TakeANumber6 er gjengitt nedenfor. Legg merke til at det for Tekstoppdaterer-objektet ikke gjøres kall på noen start-metode.

 1 /*
 2  * File: TakeANumber6.java
 3  * Author: Java, Java, Java
 4  * Description: An instance of this class serves as
 5  *  a shared resource for the customers and clerk threads
 6  *  of the bakery simulation. This object contains two
 7  *  instance variables, both of which are initialized to 0.
 8  *  The variable next represents the next place in line.
 9  *  This simulates the "ticket" given to customers as they
10  *  arrive. The variable "serving" represents the next
11  *  customer to be served. This simulates the "now serving"
12  *  display that the clerk accesses to determine who's next.
13
14  * In this version all of its methods are synchronized and
15  *  the println() statements that report the state of the
16  *  simulation have been moved into these syncrhonized methods.
17  *  Also, the nextCustomer() method forces the clerk to wait
18  *  if there are no customers.
19  */
20
21 import javax.swing.*;
22
23 public class TakeANumber6
24 {
25   private int next = 0;
26   private int serving = 0;
27   private JTextArea utskrift;
28
29   public TakeANumber6(JTextArea t)
30   {
31     utskrift = t;
32   }
33
34   public synchronized int nextNumber(int custId)
35   {
36     next = next + 1;
37     SwingUtilities.invokeLater(new Tekstoppdaterer(utskrift,
38             "\nCustomer " + custId + " takes ticket " + next));
39     notifyAll();
40     return next;
41   } // nextNumber()
42
43   public synchronized int nextCustomer()
44   {
45     try
46     {
47       while (next <= serving)
48       {
49         SwingUtilities.invokeLater(
50                 new Tekstoppdaterer(utskrift, "\nClerk waiting "));
51         wait();
52       }
53       ++serving;
54       SwingUtilities.invokeLater(
55               new Tekstoppdaterer(utskrift, "\nClerk serving ticket "
56               + serving));
57       return serving;
58     }
59     catch (InterruptedException e)
60     {
61       System.out.println("Exception " + e.getMessage());
62       return -1;
63     }
64   } // nextCustomer()
65 }

I de andre klassene er det ikke behov for andre endringer enn at de bruker den nye typen kønummerautomat. De nødvendige oppdateringer finnes i filene Clerk6.java, Customer6.java, og Bakery6.java.

Hvordan vite om noe kode utføres av tråden for hendelseshåndtering?

Noen ganger kan en være i tvil om en bestemt kodebit blir utført av tråden for hendelseshåndtering eller av en annen tråd. Da kan metodekallet

  SwingUtilities.isEventDispatchThread()

være nyttig å bruke. Dette vil returnere true dersom koden utføres av tråden for hendelseshåndtering, false ellers.

Oppdatering av brukergrensesnitt under utførelse av tidkrevende kode

Dersom du i et vindusbasert program (med swing-komponenter) skal programmere en tidkrevende oppgave, bør du rette deg etter følgende regler:

Den første regelen bør følges for å unngå at brukeren skal oppleve programmet som om det "henger" mens en tidkrevende oppgave blir utført. Tråden for hendelseshåndtering vil nemlig ikke respondere på bruker-input så lenge den er opptatt med å utføre en tidkrevende oppgave. Den andre regelen har å gjøre med måten som swing-komponentene er implementert på. Dette er allerede kommentert ovenfor. De to reglene kan se ut til å være i konflikt med hverandre, for mens en tidkrevende oppgave blir utført, vil vi kunne ønske å informere brukeren om framdriften av oppgaveutførelsen. Det er klart at dersom vi skal få til dette, må de to involverte trådene på en eller annen måte kommunisere med hverandre. En slik kommunikasjon kan være ganske vanskelig å få til å virke som vi ønsker. Heldigvis finnes det i klassebiblioteket en klasse som kan hjelpe oss til å løse problemet.

SwingWorker-klassen

Klassen SwingWorker<T, V> er laget for situasjoner der du har en tidkrevende prosess som utføres av en bakgrunnstråd samtidig som du ønsker oppdatering av brukergrensesnittet under prosessens gang og når den er ferdig. De to typeparametrene til klassen har følgende betydning:

Det er ingen restriksjoner på hva som kan brukes av typer for de to parametrene, annet enn at det må være objekttyper.

Siden SwingWorker-klassen er abstrakt, må vi definere subklasser av den for å bruke den. I subklassen må vi redefinere doInBackground-metoden. Metoden skal inneholde de oppgavene som skal utføres av bakgrunnstråden. Den produserer altså et sluttresultat av type T og progresjonsdata (delresultater) av type V. Når vi har opprettet SwingWorker-objektet og tilført det eventuelle input-data, for eksempel via konstruktør-parametre, gjør vi kall på dets execute-metode for å aktivere det. Den vil sette opp en arbeidstråd til å utføre de oppgavene som doInBackground-metoden spesifiserer. (Merk at vi gjør ikke selv kall på doInBackground.)

Metoden doInBackground skal altså produsere progresjonsdata av type V. For å formidle et slikt dataelement til brukergrensesnittet, bruker vi det som aktuell parameter i et kall på SwingWorker-klassens publish-metode. Denne vil sende dataene videre til SwingWorker-klassens process-metode, som vi ikke gjør kall på selv, men som vil bli kalt opp av tråden for hendelseshåndtering. Vi må imidlertid implementere metoden. Den må blant annet inneholde de instruksjonene vi ønsker utført for oppdatering av brukergrensesnittet. (I denne metoden refererer vi altså til komponenter i brukergrensesnittet, noe vi ikke gjør i doInBackground-metoden.) Et eksempel på dette er gitt i det etterfølgende programeksempel.

Når doInBackground-metoden er ferdig med sin jobb, vil tråden for hendelseshåndtering gjøre kall på SwingWorker-klassens done-metode. Den er ikke forhåndsprogrammert til å gjøre noe som helst. Det er derfor aktuelt å redefinere den i vår SwingWorker-subklasse. I done-metoden vil det være aktuelt å gjøre kall på SwingWorker-klassens get-metode for å få tak i sluttresultatet som doInBackground-metoden har produsert, og som altså er av type T. Det vil være aktuelt å oppdatere brukergrensesnittet med disse dataene, og eventuelt med andre data. Eksempel på dette finnes også i det etterfølgende programeksempel.

Dersom vi ønsker å avbryte en SwingWorker i utførelsen av oppgaven som dens bakgrunnstråd tar seg av, kan vi gjøre kall på SwingWorker-objektets cancel-metode med true som parameter. Dersom et slikt kall har skjedd før SwingWorker-objektet er ferdig med sitt arbeid, vil dets isCancelled-metode deretter returnere true. Eksempel på bruk av dette er også gitt i det etterfølgende programeksempel, der brukeren er gitt mulighet til å avbryte prosessen via et menyalternativ Cancel.

Programeksempel

Eksemplet er hentet fra boka Core Java av Cay S. Horstmann og Gary Cornell. Programmet gir brukeren anledning til å velge en tekstfil som programmet skal vise linje for linje i et tekstområde. For en stor fil kan dette ta en del tid. For oppgaven blir det definert en subklasse til SwingWorker-klassen. Subklassen blir plassert som en indre klasse i vindusklassen, slik at det er mulig å referere direkte til vindusklassens komponenter. Subklassens doInBackground-metode blir programmert til å foreta innlesingen, tilføre progresjonsdata til process-metoden, og returnere hele filinnholdet når alt er lest inn.

Som datatype for delresultatene (progresjonsdata), som er betegnet med V i beskrivelsen ovenfor, er det i vindusklassen definert en egen, indre klasse ProgressData:

  private class ProgressData
  {
    public int number;
    public String line;
  }

Vi ser at klassen bare definerer en innpakning rundt en tekstlinje og dens tilhørende linjenummer. For hver tekstlinje som blir lest inn blir det opprettet et slikt objekt. Det blir via publish-metoden sendt til process-metoden. Men av effektivitetshensyn vil den ikke bli kalt opp (av tråden for hendelseshåndtering) hver gang dette skjer. Isteden blir objektene lagt inn i en liste. Denne lista blir mottatt av process-metoden hver gang den blir kalt opp. Metoden er programmert til å oppdatere brukergrensesnittet med de data som foreligger så langt.

For å bygge opp den totale teksten som blir lest inn fra tekstfila, blir det brukt et objekt av type StringBuilder, som også er resultattypen for SwingWorker-objektet (betegnet med T i beskrivelsen ovenfor). En kort beskrivelse av StringBuilder-typen finnes i notatet Tekstmanipulering. I SwingWorker-klassens done-metode, som blir kalt opp (av tråden for hendelseshåndtering) når doInBackground-metoden er ferdig med sitt arbeid, blir StringBuilder-objektet som doInBackground har bygget opp, og som nå vil inneholde all teksten i hele tekstfila (i tilfelle innlesingen ikke er blitt avbrutt), blir objektet hentet fram ved hjelp av get-metoden. Brukergrensesnittet blir oppdatert på grunnlag av dets innhold. Fullstendig programkode finnes i fila SwingWorkerTest.java. Innholdet er gjengitt nedenfor.

  1 package corejava;
  2
  3 import java.awt.*;
  4 import java.awt.event.*;
  5 import java.io.*;
  6 import java.util.*;
  7 import java.util.List;
  8 import java.util.concurrent.*;
  9 import javax.swing.*;
 10
 11 /**
 12  * This program demonstrates a worker thread that runs a potentially
 13  * time-consuming task.
 14  *
 15  * @version 1.1 2007-05-18
 16  * @author Cay Horstmann
 17  */
 18 public class SwingWorkerTest
 19 {
 20   public static void main(String[] args) throws Exception
 21   {
 22     EventQueue.invokeLater(new Runnable()
 23     {
 24       public void run()
 25       {
 26         JFrame frame = new SwingWorkerFrame();
 27         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 28         frame.setVisible(true);
 29       }
 30     });
 31   }
 32 }
 33
 34 /**
 35  * This frame has a text area to show the contents of a text file,
 36  * a menu to open a file and cancel the opening process, and a status
 37  * line to show the file loading progress.
 38  */
 39 class SwingWorkerFrame extends JFrame
 40 {
 41   private JFileChooser chooser;
 42   private JTextArea textArea;
 43   private JLabel statusLine;
 44   private JMenuItem openItem;
 45   private JMenuItem cancelItem;
 46   private SwingWorker<StringBuilder, ProgressData> textReader;
 47   public static final int DEFAULT_WIDTH = 450;
 48   public static final int DEFAULT_HEIGHT = 350;
 49
 50   public SwingWorkerFrame()
 51   {
 52     chooser = new JFileChooser();
 53     chooser.setCurrentDirectory(new File("."));
 54
 55     textArea = new JTextArea();
 56     add(new JScrollPane(textArea));
 57     setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 58
 59     statusLine = new JLabel(" ");
 60     add(statusLine, BorderLayout.SOUTH);
 61
 62     JMenuBar menuBar = new JMenuBar();
 63     setJMenuBar(menuBar);
 64
 65     JMenu menu = new JMenu("File");
 66     menuBar.add(menu);
 67
 68     openItem = new JMenuItem("Open");
 69     menu.add(openItem);
 70     openItem.addActionListener(new ActionListener()
 71     {
 72       public void actionPerformed(ActionEvent event)
 73       {
 74         // show file chooser dialog
 75         int result = chooser.showOpenDialog(null);
 76
 77         // if file selected, set it as icon of the label
 78         if (result == JFileChooser.APPROVE_OPTION)
 79         {
 80           textArea.setText("");
 81           openItem.setEnabled(false);
 82           textReader = new TextReader(chooser.getSelectedFile());
 83           textReader.execute();
 84           cancelItem.setEnabled(true);
 85         }
 86       }
 87     });
 88
 89     cancelItem = new JMenuItem("Cancel");
 90     menu.add(cancelItem);
 91     cancelItem.setEnabled(false);
 92     cancelItem.addActionListener(new ActionListener()
 93     {
 94       public void actionPerformed(ActionEvent event)
 95       {
 96         textReader.cancel(true);
 97       }
 98     });
 99   }
100
101   private class ProgressData
102   {
103
104     public int number;
105     public String line;
106   }
107
108   private class TextReader extends SwingWorker<StringBuilder, ProgressData>
109   {
110     private File file;
111     private StringBuilder text = new StringBuilder();
112
113     public TextReader(File file)
114     {
115       this.file = file;
116     }
117
118     // the following method executes in the worker thread; it 
119     //doesn't touch Swing components
120     @Override
121     public StringBuilder doInBackground() throws
122             IOException, InterruptedException
123     {
124       int lineNumber = 0;
125       Scanner in = new Scanner(new FileInputStream(file));
126       while (in.hasNextLine())
127       {
128         String line = in.nextLine();
129         lineNumber++;
130         text.append(line);
131         text.append("\n");
132         ProgressData data = new ProgressData();
133         data.number = lineNumber;
134         data.line = line;
135         publish(data);
136         Thread.sleep(1); // to test cancellation; 
137         //no need to do this in your programs
138       }
139       return text;
140     }
141
142     // the following methods execute in the event dispatch thread
143     @Override
144     public void process(List<ProgressData> data)
145     {
146       if (isCancelled())
147       {
148         return;
149       }
150       StringBuilder b = new StringBuilder();
151       statusLine.setText("" + data.get(data.size() - 1).number);
152       for (ProgressData d : data)
153       {
154         b.append(d.line);
155         b.append("\n");
156       }
157       textArea.append(b.toString());
158     }
159
160     @Override
161     public void done()
162     {
163       try
164       {
165         StringBuilder result = get();
166         textArea.setText(result.toString());
167         statusLine.setText("Done");
168       }
169       catch (InterruptedException ex)
170       {
171       }
172       catch (CancellationException ex)
173       {
174         textArea.setText("");
175         statusLine.setText("Cancelled");
176       }
177       catch (ExecutionException ex)
178       {
179         statusLine.setText("" + ex.getCause());
180       }
181
182       cancelItem.setEnabled(false);
183       openItem.setEnabled(true);
184     }
185   }
186 }

Følgende bilde viser kjøring av programmet mens det er i ferd med å lese inn en fil.

Når en filinnlesing er ferdig, kan programvinduet se ut som på følgende bilde:

Merknad 1: Annotasjoner

I programmet ovenfor finnes det foran flere av metodene i den definerte subklassen til SwingWorker følgende kode:

    @Override

Dette er en såkalt annotasjon. En annotasjon gir data om et program uten å være en del av programmet selv. Annotasjoner har ingen direkte effekt på koden som de annoterer. Annotasjoner kan brukes til flere formål, slik som:

Annotasjoner kan brukes på et programs deklarasjoner av klasser, datafelt, metoder, og andre programelementer.

Annotasjoner skal stå rett foran det element de tilhører, ofte plassert på en egen linje, og de kan inneholde elementer med navngitte eller ikke-navngitte verdier, slik som i eksemplene

  @Author(
    name = "Petter Smart",
    date = "19.03.2012"
  )
  class MinKlasse ...

eller

  @SuppressWarnings(value = "unchecked")
  void minMetode(...

Dersom en annotasjon ikke har noen elementer, kan parentesene sløyfes, slik som vi har eksempler på i programkoden ovenfor:

    @Override
    public void process(List<ProgressData> data)

Denne annotasjonen brukes til å informere kompilatoren at elementet har til hensikt å redefinere et element som er definert i superklassen. Det er ikke noe krav om å bruke denne annotasjonen når vi redefinerer metoder, men den kan bidra til å hindre feil. Dersom en metode merket med @Override ikke redefinerer på en korrekt måte en metode arvet fra en superklasse, så vil kompilatoren generere en feilmelding.

Nærmere opplysninger om annotasjoner kan finnes på nettadressen http://download.oracle.com/javase/tutorial/java/javaOO/annotations.html.

Merknad 2

I doInBackground-metoden til programmet ovenfor er det lagt inn en pause på ett millisekund for hver innlesing av en tekstlinje:

136         Thread.sleep(1); // to test cancellation; 

Eneste hensikten med dette er her at programmet skal gå såpass sakte at brukeren kan rekke å prøve ut Cancel-alternativet for å stoppe innlesingen. I et normalt program ville en ikke ha lagt inn en slik pause. Dersom du kommenterer ut instruksjonen, vil du se at programmet kjører atskillig raskere.

Innholdsoversikt for programutvikling

Copyright © Kjetil Grønning og Eva Hadler Vihovde, revidert 2014