Ohjelmointitekniikka (Java) -materiaalia © Arto Wikla. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.

Ohjelmointitekniikka (Java): rinnakkaisuudesta

(Muutettu viimeksi 7.2.2008)

Rinnakkaisuus, prosessit, säikeet, ...

(s.1-2)

Animointia ilman rinnakkaisuutta

(s. 2-7)

Laaditaan ensin sovellus, jossa animoidaan pomppivaa palloa samassa säikeessä käyttöliitymän toiminnallisuuden kanssa. Pallon näkymään lisäävä metodi hoitelelee itse pallon liikuttelunkin:

    try {
       Ball ball = new Ball();
       panel.add(ball);

       for (int i = 1; i <= STEPS; i++) {
          ball.move(panel.getBounds());
          panel.paint(panel.getGraphics());  // (!!)
          Thread.sleep(DELAY);
       }
    }
    catch (InterruptedException e)
    { }
[Thread.sleep(DELAY) on luokkametodi, joka odottaa annetun määrän millisekunteja. Se voi heittää tarkistetun poikkeuksen InterruptedException. Siksi siepataan. Lause panel.paint(panel.getGraphics()) tarvitaan tavallisen panel.repaint()-kutsun sijaan, koska repaint ei pääsisi suoritukseen, kun säie vain pyörii tuossa luupissa...]

Koko ohjelma: Bounce.java. (Ohjelmassa pallon piirtäminen on toteutettu 2D-välineillä.)

Kuten havaitaan, käyttöliittymä hyytyy pallon liikkumisen ajaksi. Näin siis ei pidä ohjelmoida!

Animointi omassa säikeessään

(s. 7-13)

Laitetaan pallon liikkumisen animointi omaan säikeeseensä, niin käyttöliittymäkin pysyy toimivana. Palloja voi olla myös useita ja kukin on omassa säikeessään. Käyttöliittymätapahtumia suorittava säie event dispatch thread pääsee hoitelemaan tapahtumat.

Yksinkertainen tapa suorittaa tehtävä omassa säikeessään:

  1. Kirjoita tehtävän suorittava koodi run()-metodiin luokassa, joka toteuttaa Runnable-rajapintaluokan (vaatii vain tuon metodin):
    class OmaRunnable implements Runnable {
      public void run() {
        // Tänne algoritmi!
      }
    }
    

  2. Luo luokasta ilmentymä:
       Runnable r = new OmaRunnable();
    

  3. Luo Thread-olio eli säie oman luokan ilmentymästä:
       Thread t = new Thread(r);
    

  4. Käynnistä säie:
       t.start();
    

Palloa varten ohjelmoidaan BallRunnable:

class BallRunnable implements Runnable {
   ...
   public void run() {
     try {
        for (int i = 1; i <= STEPS; i++) {
          ball.move(component.getBounds());
          component.repaint();
          Thread.sleep(DELAY);
        }
     }
     catch (InterruptedException e)
     {}
   }
}
Aina kun nappulaa painetaan, luodaan uusi säie:
   public void addBall() {
     Ball b = new Ball();
     panel.add(b);
     Runnable r = new BallRunnable(b, panel);
     Thread t = new Thread(r);
     t.start();
   }

Koko ohjelma: BounceThread.java.

Huom: Metodia run() ei kutsuta itse! Asian hoitelee start()-metodi, joka luo säikeen ja käynnistää run()-algoritmin. [Metodia run() voi kyllä kutsua itsekin, mutta silloin algoritmi suoritetaan kutsujan omassa säikeessä!]

Muutama lisäesimerkki vanhalla Javalla (1.4)

("Vanha" tarkoittaa tässä GUI:n ohjelmointia 1.4. versiolla)

Prioriteetit:
(s. 19)

Javassa säikeellä on jokin prioriteetti, suomeksi "suosituimmuusasema" tms. (yleensä suomeksikin puhutaan toki prioriteetista). Se ohjaa ohjelman suoritusta siten, että hyvän prioriteetin omaavat säikeet saavat enemmän/useammin suoritusaikaa kuin huonon prioriteetin omaavat säikeet.

Javassa säikeen prioriteetit ovat välillä 0-10. Paras prioriteetti on Thread.MAX_PRIORITY (=10), huonoin on Thread.MIN_PRIORITY (=0). Käytettävissä on myös vakio Thread.NORM_PRIORITY (=5).

Huom: Prioriteettien toteutus on hyvin järjestelmäsidonnaista! Windows XP:ssä prioriteettitasoja on seitsemän. Linux ei ota ohjelmoijan asettamia prioriteetteja lainkaan huomioon; kaikki säikeet saavat joka tapauksessa saman prioriteetin. Seuraavaa ohjelmaa kannattaakin kokeilla jossakin muussa käyttöjärjestelmassä kuin Linuxissa...

Asetetaan pallosäikeelle prioriteetti:

   public void addBall (int priority, Color color) {
      Ball b = new Ball (canvas, color);
      canvas.add (b);
      BallThread thread = new BallThread (b);
      thread.setPriority (priority);
      thread.start ();
   }
Kutsut:
      addBall (Thread.NORM_PRIORITY - 4, Color.black);
      ...
      addBall (Thread.NORM_PRIORITY + 5, Color.red);
Koko ohjelma: BounceExpress.java.

Itsekäs säie, huonoa ohjelmointia:

Varataan säie omaan käyttöön, ns. aktiivinen odotus:

   public void run () {
      try {
         for (int i = 1; i <= 1000; i++) {
            b.move ();
            if (selfish) {
               // busy wait for 5 milliseconds
               long t = System.currentTimeMillis ();
               while (System.currentTimeMillis () < t + 5)
                  ;
            }
            else
               sleep (5);
         }
      }
      catch (InterruptedException exception)
      { }
   }
BounceSelfish.java

Pallon pompottelusovelma:

Itse sovelmasivu: OneBallApplet
Ohjelma: OneBallApplet.java

Interrupted

(s. 13-15)

Säikeen suoritus päättyy, kun sen run()-metodin suoritus päättyy. Säiettä ei voi pakottaa päättymään tuota ennen. Mutta säikeelle voidaan ulkopuolelta esittää hurskas toive: "Voisitko olla kiltti ja lopettaisit toimintasi!". Tämä tehdään kutsumalla säieolion aksessoria interrupt(). Se asettaa säikeen tilatiedon interrupted status todeksi.

Säikeen algoritmi voi halutessaan(!) tutkia, onko joku pyytänyt säiettä lopettamaan toimintansa. Tuo tehdään kysymällä "onko suorituksessa olevan säikeen interrupted status tosi".

Homma voitaisiin yrittää hoidella seuraavasti:

   while (!Thread.currentThread().isInterrupted() && duunia jäljellä) {
     tee lisää duunia
   }
Jos säikeen suoritus on kuitenkin syystä tai toisesta keskeytynyt (on esim. kutsuttu sleep-metodia, säie on blocked-tilassa, ks. seuraava kohta), tuota testiä ei päästä tekemään vaikka tahtoakin olisi!

Ongelma on ratkaistu siten, että kun interrupt()-aksessoria kutsutaan keskeytyneelle säikeelle, suorituksen keskeyttänyt kutsuttu metodi (esim sleep) päätetään poikkeuksella InterruptedException. Tässäkin tilanteessa on säikeen oma päätös, lopettaako se toimintansa vai ei!

Tyypillinen ohjelmahahmo, idiomi, on:

  public void run() {
    try {
      ...
       while (!Thread.currentThread().isInterrupted() && duunia jäljellä) {
         tee lisää duunia
       }
    }
    catch (InterruptedException e) {
      säiettä pyydettiin loppumaan sleepin tai waitin sisällä
    }
    finally {
      ...
    }
  }   // säie päättyy kun sen run()-metodi pääyttyy

Testi !Thread.currentThread().isInterrupted() ei ole tarpeen, jos säikeen algoritmin jokainen toistokerta päättyy sleep-metodin kutsuun; silloin joka toiston jälkeen mennään metodiin, joka voi johtaa poikkeukseen InterruptedException:
  public void run() {
    try {
      ...
       while (duunia jäljellä) {
         tee lisää duunia;
         Thread.sleep(delay);
       }
    }
    catch (InterruptedException e) {
      säiettä pyydettiin loppumaan sleepin sisällä
    }
    finally {
      ...
    }
  }   // säie päättyy kun sen run()-metodi pääyttyy

Huom: Kun sleep-metodi heittää tuon poikkeuksen, se samalla asettaa interrupted status -tiedon epätodeksi!

Säikeen tilat

(s. 15-18)

Seuraava äärellinen automaatti kuvaa säikeen elämänkaaren ja -mahdollisuudet:

Kuva: säikeen tilat

Säieaksessori isAlive() on tosi, jos säie on runnable- tai blocked-tilassa, epätosi jos säie on käynnistämätön tai kuollut.

Synkronoinnin tarve, synchronized

(s. 22-27, 35-41, (28-34))

Säikeet voivat jakaa ja siis manipuloida yhteisiä muuttujia. Tällä voi olla kiusallisia seurauksia.

Toteutetaan "pankkiohjelma", jossa säikeet siirtelevät arvottuja summia tililtä toiselle. Tilit ovat yksinkertaisesti taulukon alkioita. Siirron logiikka on:

   public void transfer (int from, int to, double amount) {  
     if (accounts[from] < amount) return;
     System.out.print(Thread.currentThread());
     accounts[from] -= amount;
     System.out.printf(" %10.2f from %d to %d", amount, from, to);
     accounts[to] += amount;
     System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
   }
Metodi getTotalBalance() tulostaa pankin kaikkien tilien yhteenlasketun saldon, so. taulukon alkioiden summan.

Säikeen algoritmi kirjoitetaan tuttuun tyyliin

   public void run() {  
      try {  
         while (true) {  
            int toAccount = (int)(bank.size() * Math.random());
            double amount = maxAmount * Math.random();
            bank.transfer (fromAccount, toAccount, amount);
            Thread.sleep((int) (DELAY * Math.random()));
         }
      }
      catch (InterruptedException e)
      { }

Ja samoin säikeitä luodaan ja käynnistetään jo opitulla tavalla:
     for (i = 0; i < NACCOUNTS; i++) {  
       TransferRunnable r = new TransferRunnable (b, i, INITIAL_BALANCE);
       Thread t = new Thread(r);
       t.start();
     }
Ohjelmassa siis jokainen säie liittyy tiliin, jolta rahaa otetaan. Rahaa saava tili arvotaan joka tilisiirrossa erikseen.

Koko ohjelma on tässä: UnsynchBankTest.java.

Tilejä on 100 ja kullekin talletataan alussa 1000 yksikköä rahaa. Pankin kokonaisvarallisuus on siis 100000. Kun ohjelmaa suoritetaan, alkaa tapahtua ikäviä asioita! Silloin tällöin pankin kokonaisrahamäärä on liian pieni, joskus yhden tulostusrivin keskelle ilmestyy toinen rivi... Huolestuttavaa, hyvin huolestuttavaa! Ja nämä epämääräisyydet tapahtuvat peräti raha-asioissa!

Thread[Thread-94,5,main]     343,22 from 94 to 83 Total Balance: 100000,00
Thread[Thread-75,5,main]     952,48 from 75 to 52 Total Balance: 100000,00
Thread[Thread-7,5,main]      90,29 from 7 to 90 Total Balance: 100000,00
Thread[Thread-44,5,main]     680,80 from 44 to 26 Total Balance:  100000,00
Thread[Thread-28,5,main]     158,50 from 28 to 33 Total Balance:  100000,00
Thread[Thread-9,5,main]     641,98 from 9 to 44 Total Balance:  100000,00
Thread[Thread-90,5,main]     867,90 from 90 to 75 Total Balance: 100000,00
Thread[Thread-78,5,main]Thread[Thread-45,5,main]     810,01 from 45 to92 Total Balance
:   99878,69
Thread[Thread-22,5,main]     252,49 from 22 to 62 Total Balance:  99878,69
Thread[Thread-75,5,main]     980,15 from 75 to 2 Total Balance:  99878,69
Thread[Thread-46,5,main]     856,86 from 46 to 46 Total Balance:  99878,69
Thread[Thread-69,5,main]     396,49 from 69 to 53 Total Balance:  99878,69
Thread[Thread-37,5,main]     442,57 from 37 to 96 Total Balance:  99878,69
     121,31 from 78 to 79 Total Balance:  100000,00
Thread[Thread-64,5,main]     995,56 from 64 to 78 Total Balance: 100000,00
Thread[Thread-58,5,main]     859,98 from 58 to 91 Total Balance: 100000,00
Thread[Thread-81,5,main]     688,45 from 81 to 5 Total Balance:  100000,00
Ongelmat johtuvat siitä, että säikeitä suoritetaan paloittain: prosessorin (tai prosessoreiden) aikaa jaetaan säikeiden kesken (säikeiden kannalta) mielivaltaisesti. Esimerkiksi transfer-metodin Java-lauseesta
     accounts[to] += amount;
kääntyy seuraava Bytecode-koodijakso:
    aload_0
    getfield #2;    //Field accounts:[D
    iload_2
    dup2
    daload
    dload_3
    dadd
    dastore
Yksi lysti mitä nuo välikielen operaatiot yksityiskohtaisesti tarkoittavat - oleellista on, että lauseen suorittaminen johtaa monen välikielisen operaation suorittamiseen.

Koska säikeitä suoritetaan paloittain - ns. skeduloija valitsee parhaaksi katsomallaan tavalla, mitä säiettä milloinkin suoritetaan ja kuinka paljon - säikeen suoritus voi keskeytyä missä tahansa koodin kohdassa. Sitten jokin toinen runnable-tilassa oleva säie voi vallan mainiosti päästä käpälöimään täsmälleen samoja muistialueita kuin tuo keskeytetty käsitteli. Kun kun keskeytynyt säie käynnistetään uudelleen, se jatkaa kohdasta johon keskeytyi. Mutta kästeltävät tiedot ovatkin voineet muuttua kesken lauseen suorituksen - missä vain kohdassa...

Kuva: time-slicing

Mikä nyt neuvoksi?

Javassa koodijaksoja, metodeja ja lohkoja, voidaan merkitä sellaisiksi, että jos yksi säie alkaa suorittaa tuota koodijaksoa, mikään muu säie ei pääse koodijaksoa suorittamaan ennen kuin jakson suorituksen aloittanut säie saa koko koodijakson suoritettua loppuun. Metodin (tai lohkon) "yksityinen omistusoikeus" merkitään määreellä synchronized. (Tarkkaan ottaen jokaisella oliolla on oma lukkonsa, jonka säie varaa. Kun lukko on varattu, mikään muu säie ei pääse suorittamaan mitään tämän olion synkronoitua koodijaksoa.)

Kuva: synkronoitu ja ei

Ratkaistaan edellisen tehtävän ongelma määrittelemällä tilitaulukkoa käsittelevät metodit transfer ja getTotalBalance olemaan synchronized.

Esimerkissämme pankin kokonaissaldon laskenta voidaan tehdä vaivatta. Pidetään vain huoli, että kukaan ei pääse muuttamaan tilien saldoja kesken laskennan:

   public synchronized double getTotalBalance() {
      double sum = 0;
      for (double a : accounts)
        sum += a;
      return sum;
    }
Ihan näin vähällä ei selvitä tilisiirtometodin toteuttamisessa. Saattaa nimittäin käydä niin, että haluttua rahamäärää ei voida siirtää tililtä, koska tilillä ei ole tarpeeksi rahaa. Toisaalta muut säikeet saattavat aikanaan tuoda tilille rahaa ja mahdollistaa siirron. Kun säie saa olion lukon ja aloittaa transfer-metodin suorituksen, se ensi töikseen tarkistaa, voidaanko rahansiirto toteuttaa. Jos ei voida, säie vapauttaa lukon ja siirtyy blocked-tilaan odottamaan (wait()-metodin kutsu). Kun jokin säie joskus saa oman rahansiirtonsa toteutettua, se ilmoittaa kaikille lukkoa odottaville säikeille vapauttavansa lukon (notifyAll()-metodi):

   public synchronized void transfer (int from, int to, double amount)
                                            throws InterruptedException {  
     while (accounts[from] < amount)
       wait();
     System.out.print(Thread.currentThread());
     accounts[from] -= amount;
     System.out.printf(" %10.2f from %d to %d", amount, from, to);
     accounts[to] += amount;
     System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
     notifyAll();
   }
Ohjelman hahmo (idiomi) on siis seuraavanlainen:
   public synchronized void metodi (....) throws InterruptedException {  

     while (odotellaan jotakin resurssia, jonka muut säikeet 
            tähän koodiin päästessään voivat antaa)
       wait();

     tehdään kriittiset hommat häiriöttä

     notifyAll();
   }
Metodi wait() vapauttaa olion lukon ja vie säikeen blocked-tilaan odottelemaan olion lukkoa. Metodi notifyAll() palauttaa kaikki blocked-tilassa olevat olion lukkoa odottelevat säikeet runnable-tilaan. Aikanaan jokin niistä varaa olion lukon ja ehkä jopa "täydentää resurssia", jonka seurauksena ensin mainittukin säie joskus herättämisensä jälkeen pääsee tekemään työnsä.

Huom: On olemassa myös notify()-metodi, joka siirtää jonkin lukkoa odottaven säikeen runnable-tilaan. Yleensä on turvallisinta käyttää notifyAll()-metodia, jotta kaikki lukkoa odottelevat säikeet pääsevät kilvoittelemaan lukon haltuun saamisesta. (Ks. myös API-kuvaus Object-luokan metodeista notifyAll() ja notify().)

Koko ohjelma: SynchBankTest2.java.

Olion varaamista (so. sen kaikkien synkronoitujen koodijaksojen varaamista) voi havainnollistaa puhelinkioskiesimerkillä (tämä oikeastaan on vessaesimerkki, mutta ollaan nyt korrekteja...):

Huom:

Deadlock

(s. 42-44)

Deadlock on tilanne, jossa säikeet jäävät odottamaan muilta säikeiltä jotakin resurssia, jota mikään säie ei kuitenkaan pysty tarjoamaan. Käytännössä ohjelman eteneminen siis pysähtyy!

Rinnaikkaisuuden ohjelmoinnin yksi vaikeus tulee siitä, että deadlock-tilanne voi johtua sovelluksen ominaisuuksista, ei ohjelman muodollisesta rakenteesta.

Esimerkki: Edellä nähdyssä pankkiesimerkissä tilejä oli 100, kunkin tilin alkusaldo 1000. Koska suurin siirrettävä oli myös 1000, ainakin jollakin tilillä on riittävästi rahaa siirron suorittamiseen. Eli "talous pyörii" aina viimein jotenkin.

Jos muutetaan tilanne vaikka sellaiseksi, että tilejä on 10, alkusaldo 1000, ja suurin siirrettävä määrä 2000, päädytään melko nopeasti tilanteeseen, jossa kaikki säikeet jäävät turhaan odottamaan tilinsä rahavarojen täydentymistä.

Kokeile muunnettua pankkiohjelmaa: SynchBankTest2DL.java.

Vastaava tilanne voidaan saada aikaan muuttamalla säikeet saajatiliin liittyviksi: ennemmin tai myöhemmin käy niin, että ne kaikki yrittävät ottaa tililtä, jolla ei ole riittävästi rahaa. Myös notifyAll-metodin korvaaminen notify-metodilla aiheuttaa deadlockin.

On tavallaan huolestuttavaa, että kahden ensimmäisen tapauksen havaitseminen todellakin vaatii itse sovellustilanteen tutkimista. Milloin ohjelmoija voi olla varma, että hän on analysoinut ongelmansa niin perusteellisesti, ettei deadlock ole mahdollinen?

Säieturvallisia kokoelmia

(s. 53-55)

Jotakin tietorakennetta (tai luokkaa) kutsutaan säieturvalliseksi (thread safe), jos ohjelmoijan ei tarvitse itse huolehtia sen käytön synkronoinnista.

Javassa on joukko säieturvallisia kokoelmia java.util.concurrent-pakkauksessa. Esimerkkeinä ConcurrentLinkedQueue<E> ja ConcurrentHashMap<K,V>. Tällaisten käyttäminen helpottaa sovellusohjelmoijan elämää!

Kokelmien käyttöä esitellään harjoitustehtävissä.

Huom: Myös "vanhan" Javan Vector ja Hashtable ovat säieturvallisia!

Säikeet GUI:ssa

(s. 72- )

Kuten tämän sivun alun pallonpompotteluesimerkeissä nähtiin, tapahtumankäsittelysäikeessä (event dispatch thread) ei ole järkevää suorittaa animointia tai mitään muuta aikaavievää, koska kaikki GUI-tapahtumat jäävät jonottamaan suoritusvuoroaan tuohon samaan säikeeseen. On syytä suorittaa tuollaiset operaatiot omissa säikeissään, työsäikeissä(?) (worker thread).

Swing ei ole yleisesti ottaen säieturvallinen. Tämä tarkoittaa mm. sitä, että ohjelman käynnistämät (työ-)säikeetkin pääsevät halutessaan manipuloimaan käyttöliittymäelementtejä miten tahtovat, mikä voi johtaa näkymän sekoittumiseen tai muihin ongelmiin.

Huom: Eräät Swingin käyttöliittymäelementit ovat säieturvallisia, mm. metodit JTextComponent.setText, JTextArea.insert, JTextArea.append ja JTextArea.replaceRange on synkronoitu.

Seuraavassa esimerkissä käytetään JComboBox-käyttöliittymäelementtiä, joka on vaihtelevan kokoinen alasvetolista. Listaolioon voidaan lisätä ja poistaa alkioita synkronoimattomin metodein insertItemAt(Object anObject,int index) ja removeItemAt(int anIndex).

Ohjelmoidaan ensin omaan (työ-)säikeeseen arvottujen lukujen viemistä JComboBox-olion ensimmäiseksi alkioksi ja satunnaisten alkioiden poistoja:

class BadWorkerRunnable implements Runnable {
   ...
   public void run() {  
      try {
         while (true) {
           combo.showPopup();
           int i = Math.abs(generator.nextInt());
           if (i % 2 == 0)
              combo.insertItemAt(new Integer(i), 0);
           else if (combo.getItemCount() > 0)
              combo.removeItemAt(i % combo.getItemCount());
           Thread.sleep(1);
         }
      }
      catch (InterruptedException exception) {}
   }
"Bad"-napin painallus luo aina uuden tätä algoritmia suorittavan säikeen:
      JButton badButton = new JButton("Bad");
      badButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
               new Thread(new BadWorkerRunnable(combo)).start();
            }
         });

(Koko ohjelma: SwingThreadTest.java.)
[31.1.2008: Huom: Tätä ohjelmaa ei välttämättä kannata käynnistää! Ainakin minun Linuxini se kaappasi kokonaan! ;-]

Kun vähintään kaksi säiettä käsittelee, lisää ja poistaa, tuon saman JComboBox-olion alkioita, ennemmin tai myöhemmin rakenteesta yritetään poistaa alkiota, jota siellä ei ole: combo.getItemCount() antaa koon, jota sopivasti pienemmäksi rakenne ehtii tulla ennen indeksointia combo.removeItemAt(i % combo.getItemCount()). Seurauksena on ArrayIndexOutOfBoundsException:

Exception in thread "Thread-2" java.lang.ArrayIndexOutOfBoundsException: 11 at
...
javax.swing.DefaultComboBoxModel.removeElementAt(DefaultComboBoxModel.java:149)
        at javax.swing.JComboBox.removeItemAt(JComboBox.java:733)
        at BadWorkerRunnable.run(SwingThreadTest.java:90)
        at java.lang.Thread.run(Thread.java:595)

"Single thread" -sääntö:

Vain event dispatch thread pääsee käsittelemään käyttöliittymäelementtejä.

Kun Java-ohjelma käynnistetään, pääohjelma käynnistyy "main"-säikeessä. GUI-ohjelmassa pääohjelma yleensä vain luo näkymän toiminnallisuuksineen ja asettaa näkymän näkymään (setVisible). Kun main-metodi päättyy, myös sen säie päättyy.

Kun ensimmäinen ikkuna asetetaan näkyviin, samalla syntyy uusi säie, event dispatch thread, jonka tehtävä on suorittaa kaikki tapahtumiin reagoinnit. Kaikki tapahtumankäsittelijäalgoritmit (myös repaint ja revalidate) siis suoritetaan tässä säikeessä! (Ohjelmoijalta piilossa toimii muitakin säikeitä, mm. roskienkerääjä ja tapahtumia havaitseva säie.)

Ohjeita:

Edellä nähty ongelma JComboBox-olion käsittelyssä voidaan ratkaista käyttämällä invokeLater-metodia:

class GoodWorkerRunnable implements Runnable {
...
   public void run() {  
      try {
         while (true) {  
            EventQueue.invokeLater (new 
               Runnable() {  
                  public void run () {
                     combo.showPopup();
                     int i = Math.abs(generator.nextInt());
                     if (i % 2 == 0)
                       combo.insertItemAt(new Integer(i), 0);
                     else if (combo.getItemCount() > 0)
                       combo.removeItemAt(i % combo.getItemCount());
                  }
               });
            Thread.sleep (1); 
         }
      }
      catch (InterruptedException exception) {} 
   }

(Koko ohjelma: SwingThreadTest.java.)
[31.1.2008: Huom: Tätä ohjelmaa ei välttämättä kannata käynnistää! Ainakin minun Linuxini se kaappasi kokonaan! ;-]

Swingin Timer

(Tutorial: How to Use Swing Timers, API: Class Timer)

Timer-olio luodaan konstruktorilla:

public Timer(int delay, ActionListener listener)
Timer-olio aikaansaa ensimmäisen parametrin ilmaisemalla tiheydellä (ms) tapahtumia, jotka parametrina annettu ActionListener-olio kuulee, tapahtumia joihin reagoidaan event dispatch -säikeessä.

Timer-olio käynnistetään start()-metodilla ja sen suoritus voidaan keskeyttää stop()-metodilla. Keskeytetty suoritus voidaan myös käynnistää uudelleen.

  int viive = 1000; // millisekunteja
  ActionListener taskPerformer = new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
          //... tee jokin hommeli, vaikka animoinnin askel ...
      }
  };
  Timer tikittäjä = new Timer(delay, taskPerformer);
  tikittäjä.start();
  ...
  tikittäjä.stop();

Huom: Kun ohjelman ensimmäinen Timer-olio käynnistetään, syntyy (piilossa pidetty) säie hoitelemaan Timer-olioden hallintaa. Mutta koska actionPerformed-metodit suoritetaan event dispatch -säikeessä, käyttöliittymäkomponentteja voidaan surutta manipuloida actionPerformed-metodin algoritmissa. (Kunhan vain tässäkin tapauksessa pidetään huoli, ettei algoritmin yksi suorituskerta kestä kauaa!)

Huom: Timeria voidaan käyttää myös kertasuoritukseen asettamalla ennnen käynnistystä setRepeats(false).

Toteutetaan sivun alun pallonpompotusesimerkin tapainen ohjelma Timerin avulla. Muokataan nimenomaan sitä huonoa versiota, jossa animointi suoritettiin kokonaisuudessaan ja kerralla event dispatch -säikeessä:

     ...
     addButton(buttonPanel, "Start",
       new ActionListener() {
         public void actionPerformed(ActionEvent event) {
          addBall();
        }
     });
     ...

  public void addBall() {
      final Ball ball = new Ball();
      panel.add(ball);

      int delay = 1; // milliseconds

      ActionListener taskPerformer = new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
          ball.move(panel.getBounds());
          panel.repaint();
        }
      };
      new javax.swing.Timer(delay, taskPerformer).start();

Koko ohjelma: BounceTimer.java. [Täydennetty versio vanhalla Javalla (1.4): BounceTimerPlus.java]

Mietintätehtävä: Miksi ihmeessä tämä Timer-versio on niin kovin paljon hitaampi kuin omilla säikeillä toteutettu BounceThread.java?


Takaisin kurssin sisältösivulle.