Nada

Föreläsning 5: Trådar.

Designmönstren Mediator, Singleton, Factory

När man ritar klassdiagram uppstår ofta situationen att man associerar två klasser genom att dra en linje mellan dom, men att man inte är riktigt säker på i vilken riktning associationen går. Vilket objekt är det som har det andra objektet? Det framgår om man ritar en pilspets, för då går pilen från ägaren till det ägda. Man kan också rita en romb i stjärten på pilen, det betyder samma sak.

Braude ger ett pianofabriksexempel (s 110) och ett hamnexempel (s 134) på designmönstret Mediator (s 295-301). Associationen mellan piano och pianist tycks gå lika mycket åt båda hållen - pianot har en eller flera pianister och pianisten ett eller flera pianon - men för pianofabriken är det bättre att införa orderobjekt. Varje order har då ett piano och en kund men det finns ingen direktkoppling mellan dom. På samma sätt tycks det finnas en association mellan bogserbåt och skepp, men för hamnen är det lämpligt att införa bogseringsobjekt. Varje bogsering har då ett antal bogserbåtar och ett skepp.

I designmönstren Factory och Singleton (s 147-164) har man fabriksmetoder som skapar objekt av en viss klass. Den som vill ha ett objekt av swingklassen Box kan få det så här.

     Box box = Box.createVerticalBox();
Klassen Box är därför en fabrik och eftersom dess fabriksmetoder är statiska är det en konkret fabrik. Vi ska senare se exempel på designmönstret Abstract Factory, där man först tillverkar ett konkret fabriksobjekt och sedan anropar dess fabriksmetod.

För klassen Box är det frivilligt att använda fabriksmetoden. Den som så vill kan skriva new Box(- - -) med lämpliga parametrar till konstruktorn. Designmönstret Singleton används när man vill förhindra att det skapas mer än ett objekt av en viss klass, och tricket är att göra konstruktorn private.

public class Unik                 {
  private static Unik unik = null ;
  private Unik()                  {
     - - -                        }
  public static Unik get()        {
    if(unik==null) unik=new Unik(); 
    return unik                   ;}}
Exempel på användning av singleton kan vara just fabriker, som man inte behöver flera exemplar av, krypteringsobjekt och referensobjekt till operativsystemet (kommer nästa föreläsning).

Ovanstående kod är inte trådsäker! Om flera parallella trådar körs kan det faktiskt ibland blir två stycken Unik-objekt. Förklaring kommer nedan och vi ska se att det räcker att peta in ordet syncronized i huvudet på get-metoden.

Trådar

Kuggfråga: Vilket program körs när man skriver java Pnyxtr extraord? Svar: Programmet JVM, alltså javas virtuella maskin, startas av kommandot java. JVM laddar in klassen Pnyxtr.class och gör anropet main(extraord). När denna exekveringstråd når den avslutande högermåsen i main avslutar JVM körningen, såvida inte någon annan exekveringstråd ännu pågår. All javagrafik körs i en särskild tråd, så om man öppnat något fönster avslutas inte programmet när main är slut.

Anropet System.exit(0) avbryter JVM och därmed avslutas körningen tvärt och alla trådar dör. Det går lika bra med System.exit(17), talet läggs bara i operativsystemets statusvariabel. Unixkommandot echo $status visar variabelvärdet på skärmen och konventionen är att värden större än noll betyder felavbrott.

Det finns tre tillfällen när man måste skapa flera trådar i sitt program.

En tråd är ett objekt av klassen Thread eller en klass som extends Thread. Tråden börjar exekvera när dess start-metod anropas, alltså tråd.start(); och den kod som då körs är den som finns i metoden public void run(). Själva Thread-klassen har en tom run-metod, så det vill till att ens egen trådklass ersätter den med en intressantare run-metod. Så här kan det se ut.
import java.awt.*                               ;  
import javax.swing.*                            ; 
class Nada extends JFrame                       {
  int tid=10                                    ;
  Box box = Box.createVerticalBox()             ;
  JLabel fråga = new JLabel("Vad betyder NADA?");
  JTextField svar = new JTextField(25)          ;
  JLabel tidkvar = new JLabel(tid+" s kvar")    ;
  String rätt="Numerisk analys och datalogi"    ;
//-------------inre klassen Tråd-----------------
  class Tråd extends Thread                     {
    public void run()                           {
      while(tid-->0)                            {
	try                                     {
          sleep(1000)                           ;} 
        catch(Exception e)                      {
          System.out.println(e)                 ;}
	tidkvar.setText(tid+" s kvar")          ;}
      tidkvar.setText("Tiden ute!!!")           ;
      if(svar.getText().equals(rätt)) 
        fråga.setText("Rätt svar!!")            ;
      else fråga.setText("Fel, Nada="+rätt)     ;}}
//------------konstruktorn för Nada-------------
  public Nada()                                 {
    super("Snabb på tangenterna")               ;
    box.add(fråga)                              ;
    box.add(svar)                               ;
    box.add(tidkvar)                            ;
    box.setBackground(Color.yellow)             ;
    getContentPane().add(box)                   ;
    pack()                                      ;
    show()                                      ;
    new Tråd().start()                          ;}
//------------main för Nada---------------------
  public static void main(String[] args)        {
    new Nada()                                  ;}}
Klassen Tråd finns till endast för att få in rätt run-metod i Thread-objektet. Egentligen skulle man vilja skicka över metoden som en parameter, alltså new Thread(run) men så fär man inte göra i Java. Det finns ett dock ett knep för att skicka över metoder som parametrar och det är designmönstret Command.

Designmönstret Command

Det finns en konstruktor new Thread(obj) som tar ett objekt som parameter, nämligen det objekt som innehåller den önskade run-metoden. För att kompilatorn ska gå med på detta används följande enkla gränssnitt.
 
 interface Runnable {    
   public void run();}
I Nada-exemplet skulle man slippa den inre trådklassen om man använt satsen
 
      new Thread(this).start();
som talar om att det är run-metoden i Nadaklassen som tråden ska köra. Men då måste klassen ha deklarerats så här:
 
  class Nada extends JFrame implements Runnable

Tricket har upphöjts till designmönstret Command.

 
   _________              _______
  |Commander|1          */Runnable\        En Commander kan 
  |_________|------------\________/        ta hand om flera 
                             ^             Command-objekt och 
                             |             exekvera deras run-
                                           metoder vid lämpliga
                          ___|___          tillfällen.
                         |Command|
                         |-------|
                         | run() |
                         |_______|

En lite annan användning av designmönstret ger Braude (s 314-319). Han beskriver ett sätt att införa Undo som menyalternativ genom att spara tidigare kommandon som Command-objekt.

Trådsäkerhet

Om man har flera parallella trådar kan det bli konstiga fel vid sällsynta tillfällen. Det beror på att bara en tråd exekveras i taget och kan tillfälligt avbrytas när som helst för att en annan tråd ska få köra ett tag. Om båda trådarna mekar med samma variabler är inte programmet trådsäkert.

Typfallet är om två bankomater tar ut pengar från samma konto samtidigt. Bankomaterna är klienter till bankens server och som vi sett får varje klient en egen tråd i serverprogrammet. Anta att den ena tråden gör anropet konto.taUt(2000) och den andra anropet konto.taUt(300). Vad händer om koden ser ut så här?

  private int saldo       ;
  public void taUt(int x) {
    saldo=saldo-x         ;}
Satsen ser ut som en enhet men utförs i minst två steg:
  1. Hämta värdet på variabeln saldo.
  2. Lägg tillbaka värdet minus x i saldo.
Låt oss anta att saldot är på 2000 och att båda trådarna hunnit hämta det innan första tråden lägger tillbaka värdet noll. Strax därpå lägger andra tråden tillbaka värdet 1700.
Fråga: Hur mycket förlorar banken? (Svar längst ner.)
I femtio år har man varit medveten om denna risk och många har föreslagit listiga knep för att eliminera problemet, men förgäves. Det går inte att programmera trådsäkert utan en helt ny mekanism i programspråket.

Synkronisering och tjyvstopp

En metod som är synchronized kan bara anropas av en tråd i taget. I vårt exempel blir därför den här koden trådsäker.
  private int saldo                    ;
  synchronized public void taUt(int x) {
    saldo=saldo-x                      ;}
Medan ett anrop till metoden pågår blockeras den och andra anrop får ställa sej och vänta. I själva verket finns det bara ett lås per objekt, så alla synkroniserade metoder blockeras samtidigt. Om den tråd som har nyckeln till låset mitt inne i metoden exekverar satsen wait() lämnar den ifrån sej nyckeln till någon annan tråd och ställer sej själv och väntar. När någon tråd exekverar satsen notifyAll() vaknar objektets alla väntande trådar och en av dom övertar nyckeln. Typfallet är att en tråd vill konsumera något som en annan tråd ännu inte producerat.

I mer komplicerade fall kan det i alla fall bli så att alla står och väntar på varandra, så kallat tjyvstopp (eng deadlock). I bankexemplet skulle det kunna uppträda när man vill föra över pengar mellan två konton. Två trådar kan stå inne i var sitt konto och förgäves försöka komma åt det andra kontot.

Diskningsexempel

När tre personer ska diska görs en arbetsfördelning så att en diskar och två torkar. Diskade glas går till en hållare (död eller levande) med plats för ett enda glas och torkarna tar glas från hållaren. Vi simulerar detta med tre trådar:
  
	new Diskare().start()                                ;
        new Torkare().start()                                ;
	new Torkare().start()                                ;
Diskaren diskar tjugo glas och det tar högst fem sekunder per glas. Torkning tar högst tio sekunder per glas. Glasen flyttas via hållaren.
  
    public class Diskare extends Thread                      {
	public void run()                                    {
	    for(int glaset=1;glaset<=20;glaset++)            {
		System.out.println("Diskar glas "+glaset)    ;
		slumpsov(5000)                               ; 
		hållare.put(glaset)                          ;
		System.out.println("Diskat glas "+glaset)    ;}}}
    
    public class Torkare extends Thread                      {
	public void run()                                    {
	    while (true)                                     {
		int glaset = hållare.get()                   ;
		System.out.println("Torkar glas "+glaset)    ;
		slumpsov(10000)                              ;
		System.out.println("Torkat glas "+glaset)    ;}}}
Hållarens metoder måste vara synkroniserade för att man inte ska få fel av samma typ som i bankexemplet. Men för att undvika tjyvstopp måste man ha wait och notify enligt nedan.
  
    public class Hållare                                     {
	private int glas=0                                   ;
	public synchronized int get()                        {
	    while (glas==0) try{wait();} catch(Exception e){};
	    int glaset=glas                                  ; 
	    glas=0                                           ;
	    notify()                                         ;
	    return glaset                                    ;}
	public synchronized void put(int glaset)             {
	    while (glas>0) try{wait();} catch(Exception e){} ;
	    glas = glaset                                    ;
	    notify()                                         ;}}
Här finns hela programmet Disktext.java. Kopiera gärna och kör!

Dining philosophers

Det klassiska tjyvstoppsexemplet gäller fem filosofer vid ett runt matbord. Mat finns det gott om men bara fem ätpinnar. Det förnuftiga är förstås att två i taget äter och efter varje munsbit lämnar ifrån sej pinnarna. Men om filosoferna programmeras som trådar kan dom bli sittande med en pinne var och svälta ihjäl i väntan på den andra pinnen!

Att synkronisera ätmetoden skulle visserligen fungera, men då skulle bara en filosof i taget få äta. För att få fram ett förnuftigt beteende måste man införa något extra element, till exempel en servett eller en hela rödvin som går runt enligt några smarta regler. (Tänk ut dom och meddela mej - jag har just gett upp. /Kursledaren)



Svar: Tvåtusen kronor.


Sidansvarig: <henrik@nada.kth.se>
Senast ändrad 30 mars 2004
Tekniskt stöd: <webmaster@nada.kth.se>