Innholdsoversikt for programutvikling
wait
og notify
for å koordinere tråderswing
-komponenterEn 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.
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.
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:
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 > } }
Runnable r = new MinRunnableKlasse();
Thread
-objekt som inneholder ditt
Runnable
-objekt:
Thread t = new Thread(r);
t.start();
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!
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 }
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.
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.
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.
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.
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 }
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 }
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.
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?.
En tråd kan være i en av følgende seks tilstander:
Hver av tilstandene blir kort forklart i det følgende.
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".
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.
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.
En tråd vil bli avsluttet som følge av en av følgende to årsaker:
run
-metode
avslutter på normal måte.run
-metode ble avsluttet.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.
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.
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.
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.
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.
wait
og notify
for å koordinere tråderVå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()
:notify()
:notifyAll()
: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();
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.
swing
-komponenterHovedregel: 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.
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
.
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.
Dersom du i et vindusbasert program (med swing
-komponenter) skal programmere en
tidkrevende oppgave, bør du rette deg etter følgende regler:
swing
-komponenter fra andre tråder
enn tråden for hendelseshåndtering.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
-klassenKlassen
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:
T
- resultattypen returnert av SwingWorker
-objektets
doInBackground
- og get
-metoder (når prosessen er ferdig)V
- typen brukt av SwingWorker
-objektet for å skaffe delresultater
ved bruk av sine publish
- og process
-metoder (underveis i prosessen)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.
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:
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.
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