RINNAKKAISTA OHJELMOINTIA JAVA-KIELELLÄ

Samankaltaiset tiedostot
815338A Ohjelmointikielten periaatteet

812315A Ohjelmiston rakentaminen. Asynkronisuus

Concurrency - Rinnakkaisuus. Group: 9 Joni Laine Juho Vähätalo

Projekti 1 Säikeet ja kriittisen vaiheen kontrollointi javalla

Rinnakkaisohjelmointi kurssi. Opintopiiri työskentelyn raportti

Ohjelmoinnin peruskurssien laaja oppimäärä

Rinnakkaisohjelmointi, Syksy 2006

Luento 6. T Ohjelmoinnin jatkokurssi T1 & T Ohjelmoinnin jatkokurssi L1. Luennoitsija: Otto Seppälä

Monitorit -projekti Rinnakkaisohjelmointi

Olio-ohjelmointi Javalla

Jaana Diakite Projekti 1 JAVA-Monitorit 1(13) Rinnakkaisohjelmointi Anu Uusitalo

Liite 1. Projektin tulokset (Semaforit Javassa) Jukka Hyvärinen Aleksanteri Aaltonen

Rinnakkainen ohjelmointi

Rinnakkaisuus. parallel tietokoneissa rinnakkaisia laskentayksiköitä concurrent asioita tapahtuu yhtaikaa. TTY Ohjelmistotekniikka

4. Luento: Prosessit ja säikeets. Tommi Mikkonen,

Ohjelmassa henkilön etunimi ja sukunimi luetaan kahteen muuttujaan seuraavasti:

Ohjelmoinnin perusteet Y Python

Käyttöjärjestelmät: poissulkeminen ja synkronointi

8. Näppäimistöltä lukeminen 8.1

Sisällys. 12. Näppäimistöltä lukeminen. Yleistä. Yleistä

RINNAKKAINEN OHJELMOINTI A,

Graafisen käyttöliittymän ohjelmointi Syksy 2013

5. Luento: Rinnakkaisuus ja reaaliaika. Tommi Mikkonen,

812341A Olio-ohjelmointi Peruskäsitteet jatkoa

8. Näppäimistöltä lukeminen 8.1

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

Ohjelmoinnin peruskurssien laaja oppimäärä

Harjoitus Olkoon olemassa luokat Lintu ja Pelikaani seuraavasti:

14. Poikkeukset 14.1

JAVA on ohjelmointikieli, mikä on kieliopiltaan hyvin samankaltainen, jopa identtinen mm. C++

Ohjelmointi 2 / 2010 Välikoe / 26.3

9. Periytyminen Javassa 9.1

11. Javan toistorakenteet 11.1

Metodit. Metodien määrittely. Metodin parametrit ja paluuarvo. Metodien suorittaminen eli kutsuminen. Metodien kuormittaminen

Sisällys. 14. Poikkeukset. Johdanto. Johdanto

Sisällys. 14. Poikkeukset. Johdanto. Johdanto

Java-kielen perusteet

Ohjelmoinnin perusteet Y Python

14. Poikkeukset 14.1

Javan semaforit. Joel Rybicki, Aleksi Nur mi, Jara Uitto. Helsingin yliopisto

12. Näppäimistöltä lukeminen 12.1

Ohjelmoinnin jatkokurssi, kurssikoe

JAVA-PERUSTEET. JAVA-OHJELMOINTI 3op A JAVAN PERUSTEET LYHYT KERTAUS JAVAN OMINAISUUKSISTA JAVAN OMINAISUUKSIA. Java vs. C++?

Mikä yhteyssuhde on?

2 Konekieli, aliohjelmat, keskeytykset

12. Javan toistorakenteet 12.1

Monitorit. Monitori Synkronointimenetelmiä Esimerkkejä. Andrews , Stallings 5.5

Monitorit. Tavoite. Monitori Synkronointimenetelmiä Esimerkkejä. Andrews , Stallings 5.5. Minimoi virhemahdollisuuksia

Lohkot. if (ehto1) { if (ehto2) { lause 1;... lause n; } } else { lause 1;... lause m; } 16.3

Lohkot. if (ehto1) { if (ehto2) { lause 1;... lause n; } } else { lause 1;... lause m; } 15.3

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

15. Ohjelmoinnin tekniikkaa 15.1

Informaatioteknologian laitos Olio-ohjelmoinnin perusteet / Salo

2. Olio-ohjelmoinista lyhyesti 2.1

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op. Poikkeukset ja tietovirrat: Virhetilanteiden ja syötevirtojen käsittely

ITKP102 Ohjelmointi 1 (6 op)

12. Javan toistorakenteet 12.1

Sisällys. 15. Lohkot. Lohkot. Lohkot

T Henkilökohtainen harjoitus: FASTAXON

ITKP102 Ohjelmointi 1 (6 op)

10 Lock Lock-lause

Kompositio. Mikä komposition on? Kompositio vs. yhteyssuhde Kompositio Javalla Konstruktorit set-ja get-metodit tostring-metodi Pääohjelma

5. HelloWorld-ohjelma 5.1

Luokka Murtoluku uudelleen. Kirjoitetaan luokka Murtoluku uudelleen niin, että murtolukujen sieventäminen on mahdollista.

Tietojen syöttäminen ohjelmalle. Tietojen syöttäminen ohjelmalle Scanner-luokan avulla

on ohjelmoijan itse tekemä tietotyyppi, joka kuvaa käsitettä

Sisällys. Yleistä attribuuteista. Näkyvyys luokan sisällä. Tiedonkätkentä. Aksessorit. 4.2

812341A Olio-ohjelmointi, IX Olioiden välisistä yhteyksistä

HOJ Säikeet (Java) Ville Leppänen. HOJ, c Ville Leppänen, IT, Turun yliopisto, 2012 p.1/55

Ohjelmoinnin perusteet Y Python

Java-kielen perusteita

Poikkeustenkäsittely

Sisällys. Yleistä attribuuteista. Näkyvyys luokan sisällä ja ulkopuolelta. Attribuuttien arvojen käsittely aksessoreilla. 4.2

4. Luokan testaus ja käyttö olion kautta 4.1

Luokat ja oliot. Ville Sundberg

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op. Tietorakenneluokkia 2: HashMap, TreeMap

JAVA-OHJELMOINTI 3 op A274615

58131 Tietorakenteet ja algoritmit (syksy 2015)

Arto Salminen,

Semaforit Javassa. Mari Kononow, Eveliina Mattila, Sindi Poikolainen HELSINGIN YLIOPISTO

1 Tehtävän kuvaus ja analysointi

Ongelma(t): Miten tietokoneen käyttöjärjestelmä toimii sisäisesti, jotta resurssit saadaan tehokkaaseen käyttöön?

5. HelloWorld-ohjelma 5.1

Java-kielen perusteet

Java-kielen perusteita

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. X Poikkeusten käsittelystä

4. Olio-ohjelmoinista lyhyesti 4.1

Ohjelmoinnin perusteet, kurssikoe

4. Lausekielinen ohjelmointi 4.1

Oliosuunnitteluesimerkki: Yrityksen palkanlaskentajärjestelmä

TIEP114 Tietokoneen rakenne ja arkkitehtuuri, 3 op. Assembly ja konekieli

TIEP114 Tietokoneen rakenne ja arkkitehtuuri, 3 op. Assembly ja konekieli

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Pakkaukset ja määreet

Rinnakkaisuuden hyväksikäyttö peleissä. Paula Kemppi

Sisällys. 11. Javan toistorakenteet. Laskurimuuttujat. Yleistä

Tietorakenteet (syksy 2013)

9. Periytyminen Javassa 9.1

1. Omat operaatiot 1.1

15. Ohjelmoinnin tekniikkaa 15.1

11/20: Konepelti auki

Transkriptio:

RINNAKKAISTA OHJELMOINTIA JAVA-KIELELLÄ Tässä dokumentissa esitellään Java-kielisen rinnakkaisen ohjelmoinnin perusteet kurssin 812315A Ohjelmiston rakentaminen tarpeisiin. Aluksi käsitellään lyhyesti rinnakkaista ohjelmointia yleisesti. Toisessa kappaleessa perehdytään Javan rinnakkaisuuden perusominaisuuksiin ja kolmannessa rinnakkaisessa ohjelmoinnissa esiintyviin tyypillisin malleihin tuottaja-kuluttajamalli sekä lukijakirjoittajamalli. Tämän jälkeen esitellään lyhyesti eloisuusongelmia ja viimeisessä kappaleessa tutustutaan joihinkin Javan korkeamman tason rinnakkaisuuden hallinnan mekanismeihin. Rinnakkaisuuden ominaisuuksiin käyttöjärjestelmän näkökulmasta voi tutustua esimerkiksi teoksesta [Sil]. 1. Johdanto rinnakkaisuuteen Peräkkäinen ohjelma (sequential program) koostuu toisiaan seuraavista käskyistä, joita suoritetaan peräkkäin, kunnes ohjelma päättyy. Peräkkäisellä ohjelmalla on selkeä suorituspolku ja se on deterministinen, ts. samalla syötteellä ohjelma antaa aina saman tuloksen. Rinnakkainen ohjelma (concurrent program, parallel program) sen sijaan suorittaa kahta tai useampaa toimintaa yhtä aikaa. Rinnakkaisella ohjelmalla ei ole yhtä suorituspolkua ja sen antama tulos voi riippua myös toimintojen suoritusjärjestyksestä. Lähes kaikki nykyiset tietokoneohjelmat ovat jossain määrin rinnakkaisia; graafisen käyttöliittymän toiminta perustuu nimittäin siihen, että käyttöjärjestelmä kuuntelee käyttöliittymään liittyviä tapahtumia ja ohjaa ne sitten suorittavalle ohjelmalle. Rinnakkainen ohjelma voidaan suorittaa Antamalla yhden prosessorin huolehtia eri toiminnoista (multiprogramming). Käyttöjärjestelmän on tuettava moniajoa. Antamalla kukin tehtävä eri prosessorin tai prosessoriytimen suoritettavaksi samassa koneessa (multiprocessing). Hajauttamalla eri toiminnot toisiinsa kytkettyihin koneisiin (distributed programming). Kahdessa jälkimmäisessä tapauksessa on kysymys aidosta, fyysisestä rinnakkaisuudesta. Ensimmäisessä tapauksessa rinnakkaisuus voi olla siinä mielessä harhaa, että yksi prosessori voi suorittaa vain yhden konekielisen käskyn kerrallaan. Tosin nykyään yhdessäkin prosessorissa on yleensä useampia ytimiä, jotka voivat suorittaa operaatioita aidosti rinnakkain. Siinäkin tapauksessa, että konekielisiä käskyjä suoritetaan yksi kerrallaan, käyttöjärjestelmä voi huolehtia siitä, että kukin toiminto saa keskusyksiköltä suoritusaikaa niin, että toiminnot suoritetaan rinnakkain. Tässä tapauksessa puhutaan myös loogisesta rinnakkaisuudesta. Tässä moduulissa keskitytään ensimmäisen tapauksen rinnakkaiseen ohjelmointiin (concurrent programming). 1.1. Prosesseista ja säikeistä Prosessi on käyttöjärjestelmän tapa ajaa useita ohjelmia rinnakkain yhdessä prosessorissa yleensä yhtä prosessia vastaa yksi suoritettava ohjelma. Käyttöjärjestelmä takaa prosesseille jonkinasteisen riippumattomuuden muista prosesseista. Turvasyistä prosessit eivät saa yleensä jakaa muistialuetta. Näin ollen niiden välinen kommunikaatio pitää hoitaa muilla mekanismeilla (esimerkiksi putkilla). Prosessin ominaisuudet riippuvat käyttöjärjestelmästä. Yleisimmin prosessien suorittaminen perustuu aikajakoon: kukin prosessi keskeytetään aika ajoin ja toinen prosessi päästetään suoritukseen. 1

Prosessi voi elinkaarensa aikana olla seuraavissa tiloissa: Uusi (new) prosessi on luotu. Ajossa (runnning) prosessin koodia suoritetaan. Odotustilassa (waiting) prosessin suoritus on estetty, koska se odottaa jotakin tapahtumaa (IO tapahtuma, signaali jne.). Valmiustilassa (ready) prosessi on valmiina suoritukseen, kun se saa aikaa keskusyksiköltä. Lopetettu (terminated) prosessin suoritus on lopetettu. Käyttöjärjestelmä huolehtii prosessien skeduloinnista (scheduling) eli siitä, miten valitaan valmiustilassa oleva prosessi suoritukseen. Prosessin prioriteetti määrää sen, kuinka paljon prosessi saa suoritusaikaa muihin prosesseihin verrattuna. Prosessin keskeyttäminen vaatii aina sen keskeytyshetkisten ominaisuuksien tallentamisen, jotta prosessi saataisiin uudelleen asianmukaisesti suoritukseen. Prosessin ominaisuuksia (prosessin tila, CPU rekisterien arvot, muistinhallintatiedot jne.) säilytetään ns. prosessikontrollilohkossa (Process Control Block, PCB), joka tallennetaan prosessin keskeytyessä (ks. esim. [Sil], luku 4). Näin ollen moniajo kuluttaa ylimääräisiä resursseja. Prosessi on (erityisesti UNIXissa) raskaansarjan suoritusyksikkö. Yksittäinen ohjelma saadaan kevyemmin rinnakkaiseksi jakamalla se säikeisiin. Säie on peräkkäisesti toimiva käskyjono, joka toimii muista säikeistä riippumatta; säikeellä on oma ohjelmalaskuri ja pino. Saman prosessin säikeet käyttävät kuitenkin tämän prosessin muistialuetta ja käyttöjärjestelmän resursseja, ts. ne voivat käsitellä samoja tiedostoja sekä oheislaitteita. Säikeille annetaan suoritusaikaa samaan tapaan kuin prosesseillekin, paitsi että aikajako suoritetaan ohjelman saaman ajan puitteissa ja säikeet priorisoidaan ohjelmallisesti. Säikeiden yhteydenpito voi tapahtua kuten prosessienkin, mutta lisäksi niillä on yhteinen muistialue, jota voidaan käyttää kommunikaatiossa. Säikeen luominen kuluttaa aikaa keskimäärin kolmasosan uuden prosessin luomisesta. Säikeiden luomiselle ja synkronoinnille käyttöjärjestelmätasolla on olemassa standardi (POSIX : IEEE standardit, osa 1003.1). Standardissa kuvataan säikeen käyttäytyminen, ei kuitenkaan itse toteutusta. Standardia luotaessa 1990-luvun puolivälissä syntyi myös C-kielinen standardikirjasto Pthreads (POSIX threads). Tässä moduulissa keskitytään käytännön toteutuksessa Javan säikeisiin, jotka pääsääntöisesti noudattelevat mainittua standardia. Joitakin poikkeuksia tosin on, erityisesti säikeiden odottamisen (waiting) ja ilmoittamisen (notification) toteutuksessa. Huomaa, että jokainen ohjelma koostuu ainakin yhdestä säikeestä, kaikissa ohjelmissa on pääohjelma, joka on säie. 2

1.2. Rinnakkaisuuden ongelmia Ohjelman kirjoittaminen rinnakkaiseksi tuo mukanaan monia etuja, ohjelman vasteajat saadaan optimoitua, ohjelma voi suorittaa IO-toimintoja samaan aikaan laskennallisten operaatioiden kanssa, ohjelma voi kommunikoida useiden muiden ohjelmien kanssa jne. Rinnakkaisuus tuo myös mukanaan useita ongelmia, joita ei esiinny peräkkäisissä ohjelmissa. Rinnakkaisen ohjelman oikeellisuuskriteerit voidaan jakaa kahteen pääkategoriaan (ks. [Lea] 1.3): Turvallisuus (safety) o Olioille ja muuttujille ei tapahdu mitään epätoivottua Eloisuus (liveness) o Kaikki tarkoitetut toimenpiteet suoritetaan joskus 1.2.1. Turvallisuus Rinnakkaisessa ohjelmoinnissa hyvin yksinkertaisetkin operaatiot voivat mennä vikaan suoritusjärjestyksen muuttuessa. Oletetaan esimerkiksi, että kaksi säiettä S1 ja S2 suorittavat samalle kokonaislukumuuttujalle x operaation x = x+1. Toivottu lopputulos on muuttujan x arvon kasvaminen kahdella. Kuitenkin tietokone suorittaa operaation x = x+1 niin, että muuttujan arvo luetaan ensin muistipaikasta rekisteriin, rekisterin arvoa kasvatetaan ja sitten uusi arvo kopioidaan rekisteristä muistipaikkaan. Näin ollen operaatio ei ole atomaarinen. Jos säikeet S1 ja S2 suorittavat operaationsa peräkkäin, saadaan haluttu lopputulos, mutta koska S1 ja S2 toimivat toisistaan riippumatta, voi suoritusjärjestys olla esimerkiksi seuraava: S1 : lue x S2 : lue x S1 : x = x+1 S1 : kirjoita x S2 : x = x+1 S2 : kirjoita x Operaation tuloksena muuttuja x kasvaa vain yhdellä, mikä ei ole toivottu lopputulos. Vastaavalla tavalla metodi voi palauttaa ennalta-arvaamattomia arvoja jos kaksi säiettä kutsuu sitä rinnakkain. Metodia, joka toimii millä tahansa kutsujärjestyksellä oikein, sanotaan säieturvalliseksi (thread safe). Edelleen muuttujan tai muun tallennusrakenteen samanaikainen kirjoittaminen ja lukeminen voi tuottaa kummallisia arvoja, samoin kuin kahden säikeen yhtäaikainen kirjoittaminen samaan muuttujaan. Tällaisia virhetilanteita sanotaan kilpailutilanteiksi; englanninkielisissä lähteissä edellä mainituista virhetilanteista käytetään nimitystä race condition. 3

1.2.2. Eloisuus Turvallisen, ts. konflikteista vapaan, ohjelman voi kirjoittaa käyttämällä ainoastaan yhtä säiettä. Tällä tavalla menetetään kuitenkin rinnakkaisuuden edut. Näin ollen turvallisuus ja eloisuus ovat usein suunnittelussa vastakkaisissa vaakakupeissa. Eloisassa ohjelmassa kaikki aiotut operaatiot suoritetaan joskus; huomaa että mitään aikarajaa ei kuitenkaan aseteta. Reaaliaikaisessa ohjelmoinnissa on yleensä asetettu maksimiaikoja operaatioiden kestolle. Yleisimmin operaatio jää suorittamatta, koska sen säie odottaa jonkin resurssin vapautumista. Tällaisia tapauksia voivat olla: toinen säie on lukinnut metodin, jota yritetään suorittaa, metodi lukkiutuu odottaen toisen säikeen suorittamaa tapahtumaa, metodi odottaa IO-toimintoa (toisesta prosessista tai laitteesta), jotkin toiset säikeet tai ohjelmat käyttävät kaikki keskusyksikköresurssit. Lisäksi operaatio saattaa jäädä suorittamatta jonkin ajonaikaisen virheen tai poikkeuksen takia. Listassa mainituista ongelmista selvitään yleensä yrittämällä suorittaa operaatio, kunnes se onnistuu. Joskus kuitenkin ohjelma voi ajautua tilaan, jossa operaation suoritus on mahdoton pysyvästi. Tällainen tilanne voi olla Lukkiutuminen (deadlock) o Säie S1 on lukinnut olion X ja yrittää lukita oliota Y; samanaikaisesti säie S2 on lukinnut olion Y ja yrittää lukita oliota X. Tällöin kumpikaan säie ei pääse etenemään. Tämä voi tapahtua myös useammalle säikeelle syklisesti. Signaalin menettäminen (missed signal) o Säie jää odottamaan, koska se on alkanut odottaa vasta sen jälkeen kun sen heräämiseksi tarkoitettu signaali on tuotettu. Sisäkkäisten monitorien lukkiutuminen (nested monitor lockout) o Säie itse on lukinnut metodin, jota tarvitaan sen herättämiseksi. Vauhkoontuminen (livelock) o Säie yrittää toistaa operaatiota, joka on tuomittu epäonnistumaan. Nälkiintyminen (starvation) o Säie ei saa lainkaan keskusyksiköltä ajoaikaa (voi johtua useista syistä). Hajauttamisen epäonnistuminen (distributed failure) o Kone, johon ollaan yhteydessä verkon yli, kaatuu tai yhteys siihen katkeaa. Eloisuuteen liittyy läheisesti suorituskyky, jota voidaan mitata erilaisin kriteerein. Vaikka tarkkoja aikarajoja ohjelman suorittamille operaatioille ei asetettaisikaan, on selvää, että lähes jokaisen ohjelman on noudatettava jonkinlaisia suorituskykykriteerejä. Suorituskyvyn mittareita ovat esimerkiksi Tuotos (throughput) o Aikayksikössä suoritettujen operaatioiden lukumäärä. Usein esitetään myös operaatioon kuluvana aikana. Viive (latency) o Viestin lähettämisen ja sen suorittamisen välinen aika. Yleensä käytetään mittaamaan vasteaikaa. Kapasiteetti (capacity) 4

o Yhtäaikaisesti suoritettavien tehtävien lukumäärä. Erityisesti verkkopohjaisissa sovelluksissa voidaan käyttää mittaamaan palvelun saatavuutta. Tehokkuus (efficiency) o Tuotoksen suhde laskentaresursseihin. Skaalattavuus (scalability) o Suhde jolla tuotos paranee, kun resursseja lisätään. Huonontuminen (degradation) o Suhde, jolla viive kasvaa tai tuotos huononee, kun asiakkaiden, operaatioiden tai toimintojen määrää kasvatetaan lisäämättä resursseja. Näistä skaalattavuus ja huonontuminen ovat hieman epämääräisiä käsitteitä eivätkä ole suoraan vertailukelpoisia ohjelmien välillä. Monisäikeinen ohjelma on yhdellä suoritinyksiköllä peräkkäistä ohjelmaa huonompi laskentateholla mitattuna, koska säikeiden luominen ja ylläpitäminen vievät resursseja. Näin ollen ohjelmaa, joka suorittaa vain yhtä laskennallista tehtävää, jota ei voi hajauttaa monessa prosessorissa tai ytimessä suoritettavaksi, ei kannata kirjoittaa rinnakkaiseksi. Sen sijaan ohjelma, jonka on kommunikoitava käyttäjän tai oheislaitteiden kanssa suoriutuu lähes aina rinnakkaisena tehtävästään paremmin kuin peräkkäisenä. Tärkeintä rinnakkaisten ohjelmien hallinnassa on ymmärtää rinnakkaisen ohjelman ja peräkkäisen ohjelman erot ja ne ongelmatilanteet, jotka rinnakkaisuus tuo mukanaan. On osattava kiinnittää huomio ohjelmien vaatimuksiin ja rajoituksiin ja on tarkasteltava onko jokin edellä mainituista virhetilanteista mahdollinen. 2. Rinnakkaisuuden perustoteutus Javassa Rinnakkaisia ohjelmia voidaan toteuttaa useilla ohjelmointikielillä. Java-kielellä on moniin muihin yleisiin kieliin verrattuna se etu, että se on alun perin suunniteltu tukemaan rinnakkaista ohjelmointia. Näin ollen rinnakkaisuus on Javassa sisäänrakennettuna ja rinnakkaisten ohjelmien kirjoittaminen sillä on luontevaa. Siksi tässä moduulissa käytetään ohjelmointikielenä Javaa. Lukijan oletetaan tuntevan Java-kielen ja olio-ohjelmoinnin perusteet. Seuraavassa esitetään rinnakkaisuuden toteuttamista varten välttämättömät Java-kielen rakenteet, joita ei oleteta tunnetuksi. 2.1. Java-ohjelman suorittamisesta Tietokoneohjelma on tietokoneen prosessorin ymmärtämien, binäärisessä muodossa olevien käskyjen joukko. Käskykanta on prosessorikohtainen, joten tekstimuodossa olevasta koodista on käännettävä konekielinen ohjelma. Tästä huolehtii tietokoneohjelma, jota kutsutaan kääntäjäksi. Tavallinen käännettävä ohjelma muodostetaan siis seuraavasti: OHJELMA.C kääntäjä OHJELMA.EXE Näin saatava suoritettava ohjelma on aina ympäristösidonnainen ja esimerkiksi PC:ssä käännettyä ohjelmaa ei voi suorittaa UNIX-ympäristössä. Javassa siirrettävyysongelma on ratkaistu niin, että tekstimuotoisesta koodista käännetään ensin tavukoodi, joka on laitteistoriippumaton. Tavukoodi talletetaan class-tunnisteisiin tiedostoihin niin, 5

että yksi tiedosto sisältää yhden luokan koodin. Javan virtuaalikone (Java Virtual Machine, JVM) muodostaa tavukoodista suoritettavan ohjelman (ks. esim. [Ven]). Jokaisessa ympäristössä on oma virtuaalikoneensa, joka suorittaa koodin; tämä mahdollistaa siirrettävyyden, mutta tekee myös Java -ohjelmista tyypillisesti hieman hitaampia kuin esimerkiksi C-ohjelmista. Seuraava kuvio havainnollistaa mekanismia, jolla lähdekielisestä Java-koodista muodostetaan suoritettava ohjelma: Kääntäjä LÄHDE- KOODI Luokkien lataaja Suorittaja (Execution engine) (Java compiler) (Class loader) TAVU- KOODI KONEKIELINEN OHJELMA Tavukoodin tarkastaja (Bytecode verifier) Kuva 1: Java-ohjelman suorittaminen Ohjelmaa suoritettaessa virtuaalikoneen luokkien lataaja (class loader) hakee ensin ohjelmaan liittyvien luokkien tavukoodit. Tämän jälkeen ne syötetään tavukoodin tarkastajalle (bytecode verifier), joka varmistaa, että tavukoodi on asianmukaisesti muodostettu ja voidaan suorittaa turvallisesti. Lopulta suorittaja (execution engine) tulkitsee ja suorittaa tavukoodin. Suorittaja voi olla tulkkipohjainen, mutta useimmissa toteutuksissa tulkkauksen hitautta suoritettavaan konekieliseen koodiin verrattuna kompensoidaan ns. JIT-käännöksellä (Just In Time compilation). JIT-kääntäjä tulkitsee metodin tavukoodin ympäristönsä konekieliseksi koodiksi välittömästi ennen kuin koodi suoritetaan ja tallentaa konekielisen koodin välimuistiin. Näin ollen metodia uudelleen kutsuttaessa voidaan suoraan käyttää sen konekielistä koodia. Javan virtuaalikone huolehtii myös rinnakkaisuuden hallinnasta, ts. säikeiden luomisesta ja niiden ajoituksesta jne. Tällaisten seikkojen toteutus on kuitenkin käyttöjärjestelmästä riippuva (siis siitä miten käyttöjärjestelmän tasolla toteutetaan säikeet ja niiden synkronointi), sillä Javan virtuaalikone voi joissakin järjestelmissä käyttää suoraan järjestelmän säikeitä. Näin ollen rinnakkaisten ohjelmien toiminnassa voi eri alustoilla olla tästä johtuvia eroja myös eri Java-toteutuksissa. Kaikissa ympäristöissä, joissa Java on toteutettu, on kuitenkin mahdollista rakentaa rinnakkaisia ohjelmia. 6

2.2. Javan säikeet Koska säikeet kuuluvat Javan perustoteutukseen, lähes kaikissa asianmukaisissa Java-kielen oppikirjoissa käsitellään myös monisäieohjelmointia, ks. esimerkiksi [Arn] tai [Ves]. Verkossa on myös lukuisia esityksiä Javan rinnakkaisen ohjelmoinnin perusteista, esimerkiksi sivut http://docs.oracle.com/javase/tutorial/essential/concurrency/ ja http://www.tutorialspoint.com/java/java_multithreading.htm ovat tutustumisen arvoisia. Käyttöjärjestelmä voi tukea säikeitä kahdella tavalla: käyttäjätason toteutuksessa puhutaan käyttäjäsäikeistä (user threads) ja käyttöjärjestelmän ytimen tasolla ydinsäikeistä (kernel threads). Käyttäjäsäikeille tarjoaa tuen säiekirjasto, joka hoitaa säikeiden ajastuksen niin, että käyttöjärjestelmän ytimen ei tarvitse puuttua asiaan. Käyttöjärjestelmän ydin tukee suoraan ydinsäikeitä, ts. ydin luo säikeet, ajastaa ja hallitsee niitä. Useimmat nykyiset käyttöjärjestelmät käyttävät ydinsäikeitä. Koska Javassa säikeitä hallitsee Javan virtuaalikone, Javan säikeet eivät varsinaisesti kuulu kumpaakaan yllä mainittuun päätyyppiin. Säikeiden toteuttamista käyttöjärjestelmätasolla ja sen yhteyttä Javan säikeisiin käsitelllään tarkemmin teoksessa [Sil], luku 5. Javan säikeet jaetaan kahteen luokkaan: Käyttäjäsäikeet (user threads) o Varsinaiset säikeet Taustasäikeet (daemon threads) o Taustalla toimivat säikeet Lisäksi jokaisella säikeellä on prioriteetti; korkean prioriteetin säikeet saavat suorituksessa etusijan alhaisen prioriteetin säikeisiin nähden. Kun Javan virtuaalikone käynnistyy, on yleensä yksi käyttäjäsäie toiminnassa (käynnistetyn pääohjelman säie). Lisäksi virtuaalikone suorittaa yhtä tai useampaa taustasäiettä hoitamassa taustalla toimivaa roskankeruuta jne. Virtuaalikone suorittaa säikeitä, kunnes ohjelma lopetetaan kutsumalla hyväksytysti Runtime-luokan exit-metodia tai kun kaikki käyttäjäsäikeet ovat lopettaneet toimintansa. Javassa uuden säikeen voi luoda kahdella eri tavalla: Kirjoittamalla luokka, joka perii Thread-luokan, o Luokan run-metodi on uudelleenmääriteltävä, Kirjoittamalla luokka, joka toteuttaa Runnable-rajapinnan, o Luokassa on toteutettava run-metodi. Nämä tavat ovat lähes ekvivalentit; tässä käytetään kumpaakin tapaa, yleensä kuitenkin ensimmäistä. Jälkimmäinen tapa on hieman kevyempi ja sallii periytymisen. 2.2.1. Säikeen luominen Javassa Thread-luokan oliot kontrolloivat säikeitä. Uuden säikeen luominen aloitetaan uuden Thread-olion luomisella; tämän jälkeen säie konfiguroidaan (annetaan sille prioriteetti, nimi jne voidaan tehdä luomisen yhteydessä) ja lopulta säie laitetaan suoritukseen kutsumalla sen startmetodia. Huomaa, että säikeen toiminta kirjoitetaan run-metodiin, mutta ajo aloitetaan startmetodilla, jolloin järjestelmä kutsuu run-metodia. Säiettä suoritetaan, kunnes run-metodista 7

palataan, alati toimivan säikeen voi siis tehdä kirjoittamalla run-metodiin ikuisen silmukan. Seuraavassa esimerkissä toimii kaksi säiettä rinnakkain: class DemoThread extends Thread private int delay; private String line; public DemoThread(String name,string tosay,int dtime) super(name); line = tosay; delay = dtime; public void run() while(true) System.out.println(this.getName() + " says: " + line); try Thread.sleep(delay); catch(interruptedexception ie) return; public class EkaTredit public static void main(string [] argv ) DemoThread DT1 = new DemoThread("Eka jouhi","hui!",300); DemoThread DT2 = new DemoThread("Toka jouhi","hai!",600); DT1.start(); DT2.start(); Pääohjelmassa luodaan kaksi säiettä, jotka tulostavat nimensä ja annetun lausahduksen ja vaipuvat uneen annetuksi ajaksi. Suoritettaessa ohjelmaa konsolille tulostuu noin kaksi HUI! :ta yhtä hai! :ta kohti. Sama toiminto saadaan aikaan käyttämällä Runnable-rajapintaa seuraavasti: class DemoRunnable implements Runnable private int delay; private String objname; private String line; public DemoRunnable(String name,string tosay,int dtime) objname = name; line = tosay; delay = dtime; 8

public void run() while(true) System.out.println(objName + " says: " + line); try Thread.sleep(delay); catch(interruptedexception ie) return; public class EkaRun public static void main(string [] argv ) DemoRunnable DR1 = new DemoRunnable( "Eka olio","hui!",300); DemoRunnable DR2 = new DemoRunnable( "Toka olio","hai!",600); new Thread(DR1).start(); new Thread(DR2).start(); Huomaa, että koodi on lähes samanlainen kuin ensimmäisessä ohjelmassa, paitsi että pääohjelmassa luodaan uudet säikeet, joissa Runnable-oliot suoritetaan. Säikeet luodaan antamalla niille luomisen yhteydessä parametrina Runnable-olio. 2.2.2. Säikeen lopettaminen Säikeen lopettaminen on syytä tehdä palaamalla säikeen run-metodista. Tämä vaatii kuitenkin ohjelmoijalta hieman toimia: yleensä käytetään säikeen keskeyttämistä (interrupt) kutsumalla säikeen interrupt()-metodia. Tämä metodi ei suoraan lopeta säikeen toimintaa vaan laittaa säikeen keskeytystilaan, jota voidaan säikeeltä kysyä isinterrupted()-metodilla. Näin vältetään säikeen väkivaltainen katkaiseminen, joka voi johtaa olioiden korruptoitumiseen. Esimerkiksi ison tiedoston avaamisen tai lataamisen peruutus on syytä tehdä mainitulla tavalla. Esimerkki säikeen keskeyttämisestä: //Määritellään säieluokka class Interruptible extends Thread public void run() while(!this.isinterrupted()) // Tee jotain // Lopputoimenpiteet //Pääohjelmassa koodi: Interruptible irthread = new Interruptible(); irthread.start(); // Toimenpiteet // Lopetetaan irthread irthread.interrupt(); 9

Säikeen keskeyttäminen ei siis automaattisesti vaikuta toimivan säikeen suoritukseen. Koska nukkuva (sleep) tai odottava (wait) säie ei ole parhaillaan suorituksessa, sitä ei voi suoraan keskeyttää. Tällaisen säikeen keskeyttäminen aiheuttaa siksi poikkeuksen InterruptedException heittämisen. Tämän vuoksi aina, kun säie nukutetaan tai laitetaan se odottamaan, on kyseinen poikkeus otettava kiinni tai heitettävä se eteenpäin. 2.2.3. Säikeiden prioriteetti ja säieryhmät Jokaisella Javan säikeellä on prioriteetti, joka säätelee säikeen saamaa keskusyksikköaikaa suhteessa muihin säikeisiin. Mikäli kaksi säiettä on yhtä aikaa kilpailemassa keskusyksikköajasta, korkeamman prioriteetin säie voittaa. Saman prioriteetin säikeet saavat keskimäärin saman verran aikaa keskusyksiköltä. Järjestelmästä riippuu mitkä ovat eri prioriteettien säikeiden suhteelliset keskusyksikköajat. Säikeen prioriteettiin voi vaikuttaa ohjelmallisesti ennen säikeen käynnistämistä, mutta säikeen prioriteettia ei voi nostaa sen säieryhmän maksimiprioriteettia korkeammalle. Javan säikeiden prioriteetit ovat kokonaislukuja väliltä Thread.MIN_PRIORITY Thread.MAX_PRIORITY. Ellei säikeelle anneta erikseen prioriteettia, se saa arvon Thread.NORM_PRIORITY. Jokainen Javan säie kuuluu täsmälleen yhteen säieryhmään (ThreadGroup). Kun säie perustetaan, se liitetään perustavan säikeen ryhmään, mikäli erikseen ei toisin mainita. Säieryhmät voivat sisältää toisia säieryhmiä jäseninään. Kun Javan virtuaalikone käynnistyy, perustetaan järjestelmäsäieryhmä, jonka jäseniä kaikki perustettavat säieryhmät ovat. Näin ollen Javasovelluksen säieryhmät muodostavat puurakenteen, jonka juuri on järjestelmäsäieryhmä. Kun sovellus käynnistyy, virtuaalikone perustaa pääsäieryhmän systeemisäieryhmän jäseneksi. Aluksi pääsäieryhmässä on vain pääsäie, joka suorittaa pääohjelmaa. Käyttäjän luomat säikeet lisätään pääsäieryhmään; käyttäjä voi kuitenkin luoda uusia säieryhmiä ja niihin uusia säikeitä halutessaan. Säieryhmillä voi hallita säikeitä paremmin, koska jokaiselta säikeeltä voi kysyä, mihin säieryhmään se kuuluu ja käyttää sitten säieryhmien operaatioita tähän ryhmään. Säieryhmälle voidaan esimerkiksi asettaa prioriteetin maksimiarvo, jolloin minkään ryhmään kuuluvan säikeen prioriteetti ei saa ylittää kyseistä arvoa. Tässä ei kuitenkaan perehdytä tarkemmin säieryhmien hallintaan. 2.2.4. Säikeen elinkaari Javan säie voi olemassaolonsa aikana olla kuudessa eri tilassa (ks. http://docs.oracle.com/javase/8/docs/api/java/lang/thread.state.html): Uusi (NEW) Säie on luotu, mutta sen start-metodia ei ole kutsuttu, joten se ei voi vielä toimia. Ajettava (RUNNABLE) Säie voi toimia, heti kun se saa keskusyksiköltä aikaa suorittaakseen operaatioitansa. Estetty (BLOCKED) Säikeen toiminta on estetty, koska se odottaa monitorin lukon vapautumista. Odottaa (WAITING) Säie odottaa kunnes toinen säie suorittaa määrätyn toimenpiteen. Odottaa ajastetusti (TIMED_WAITING) Säie odottaa kunnes toinen säie suorittaa määrätyn toimenpiteen tai kun annettu aika on kulunut. 10

Lopetettu, kuollut (TERMINATED) Säikeen suoritus on loppunut eli sen run-metodi on päättynyt. Kun uusi säie luodaan, se saadaan ajettavaksi kutsumalla sen start-metodia. Tällöin se menee ajettavaan tilaan. Ajettavat säikeet ovat joko toiminnassa tai jonossa odottamassa keskusyksikköaikaa. Säie pysyy ajettavana, kunnes se menee odottamaan, sen toiminta estetään tai se kuolee. Estetty säie voi jälleen mennä ajettavaan tilaan, kun sen estävä seikka lakkaa vaikuttamasta. Kuollutta säiettä ei sen sijaan voi enää herättää eloon. Tuonnempana käsitellään tarkemmin, miten säie voi joutua yllä mainittuihin tiloihin. Huomautus. Säikeen nukuttaminen ei vapauta lukkoa, mikäli säikeellä on sellainen hallussaan. Näin ollen synkronoitu metodi voi sisältää sleep-lauseen ja silti estää toisten synkronoitujen metodien suorittamisen samanaikaisesti. Synkronointia käsitellään myöhemmin. 2.2.5. Säikeiden yhdistäminen Koska säikeet käyttävät yhteistä muistialuetta, ne voivat kommunikoida yhteisten muuttujien ja olioiden välityksellä. Usein kuitenkin joudutaan tilanteisiin, joissa (esimerkiksi lukkiutumisen vaaran takia) säikeet joutuvat odottamaan toisten säikeiden suorittamia operaatioita ennen suorituksen jatkamista. Tähän on kaksi perusmekanismia: Säikeiden yhdistymisen odottaminen ja oliopohjainen odottaminen. Käsitellään nyt säikeiden yhdistymisen odottaminen. Thread-luokassa on metodi join(), jonka avulla säie voi odottaa toisen säikeen suorituksen loppumista. Yksinkertainen esimerkki tällaisesta on: class JoinThread extends Thread private int delay; public JoinThread(String name,int dtime) super(name); delay = dtime; public void run() try Thread.sleep(delay); catch(interruptedexception ie) return; System.out.println(this.getName() + " finished "); 11

public class JoinTest public static void main(string [] argv ) JoinThread DT1 = new JoinThread("Eka jouhi",4000); JoinThread DT2 = new JoinThread("Toka jouhi",2000); try DT1.start(); DT2.start(); DT1.join(); DT2.join(); catch(interruptedexception ie) ; System.out.println("Program finished"); Tässä pääohjelman säie joutuu odottamaan kunnes kaksi luotua säiettä lopettavat toimintansa. Näin ollen ohjelma tulostaa noin kahden sekunnin kuluttua: Toka jouhi finished ja jälleen tästä kahden sekunnin kuluttua: Eka jouhi finished Program finished Jos pääohjelmasta kommentoidaan join-lauseet pois, niin heti tulostetaan Program finished ja vasta säikeiden päättymisen jälkeen säikeiden lopettamisviestit. Tätä menetelmää voidaan käyttää rinnakkaisessa ohjelmoinnissa esimerkiksi kun ohjelman suoritus hajautetaan useaan säikeeseen ja lopputulosta varten tarvitaan kaikkien säikeiden tulokset. Thread-luokassa on myös metodi yield(), jota kutsuttaessa säie luopuu suoritusvuorostaan ja antaa muiden säikeiden mennä suoritukseen. 2.3. Synkronointi Javassa Ohjelman oikeellisuuden varmistamiseksi säikeiden on yleensä jaksotettava toimintansa niin, että ongelmatapauksia ei ilmene. Tätä sanotaan säkeiden synkronoinniksi (synchronization), joka voi ilmetä kilpailun synkronointina (competition synchronization) ja yhteistoiminnan synkronointina (cooperation synchronization). Kilpailun synkronointia tarvitaan, kun kaksi säiettä yrittää toisistaan riippumatta käyttää jotakin resurssia ja on vaara, että tästä aiheutuu resurssin tilan rikkoutuminen. Kilpailun synkronointi toteutetaan yleensä siten, että säie pyytää joltakin synkronointioliolta resurssin käyttöoikeutta ja vapauttaa resurssin jälleen muiden säikeiden käyttöön lopetettuaan operaationsa. Yhteistoiminnan synkronointia taas tarvitaan, kun jonkin säikeen A operaation suorittaminen riippuu toisen säikeen B toiminnasta, jolloin säie A joutuu odottamaan, kunnes säie B saa suoritettua operaationsa. Synkronoinnin onnistuminen taataan yleensä jollakin rakenteellisella mekanismilla; Javan sisäänrakennettu synkronointimekanismi on ns. monitori. Monitori on olio, joka kapseloi halutut 12

toiminnot sisäänsä niin, että niitä pääsee suorittamaan vain monitorin suostumuksella. Vain yksi säie voi kerrallaan suorittaa toimintoja tällöin sanotaan että säikeellä on hallussaan monitorin lukko (lock). Monitorilla on myös (vähintään yksi) odotusjoukko (wait set), johon sijoitetaan toimintoja suorittamaan haluavat säikeet. Kun lukkoa hallussaan pitävä säie luopuu siitä, jokin odotusjoukon säie saa lukon haltuunsa ja oikeuden suorittaa monitorin valvomia toimintoja. Säie luopuu lukosta joko poistumalla monitorista lopettamalla operaation suorituksen tai ilmoittamalla (signal, notify) jollekin odotusjoukon säikeelle. Monitorin yleisimmän määritelmän perusteella monitorilla voi olla useita odotusjoukkoja, joihin odottavat säikeet sijoitetaan ehtomuuttujien (condition variables) perusteella. Tällöin säikeen on odottamaan mennessään ilmoitettava, mistä ehtomuuttujasta odottaminen riippuu, ja ilmoittavan säikeen on kohdistettava ilmoituksensa tiettyyn ehtomuuttujaan. Lisäksi monitoriin sisältyy usein broadcast-tyylinen ilmoittaminen, jossa ilmoitus annetaan kaikille odotusjoukon säikeille. Javassa mikä tahansa olio voi toimia monitorina. Näin ollen monitorit ovat luonnollinen mekanismi Java-kielisessä rinnakkaisohjelmoinnissa ja ovat lähes aina vähintään implisiittisesti esillä. Javan monitoritoteutuksessa ei käytetä ehtomuuttujia; näin ollen oliolla on aina vain yksi odotusjoukko ja siten loogisesti yksi ehtomuuttuja. Ehtomuuttujien käyttö onnistuu kuitenkin Javan kehittyneemmillä, myöhemmin kieleen lisätyillä ominaisuuksilla. 2.3.1. Kilpailun synkronointi Rinnakkaisten säikeiden käyttö voi siis johtaa moniin ongelmiin, joista yksi on datan turmeltuminen. Olion data voi turmeltua kahden eri säikeen suorittaessa samanaikaisia operaatioita olioon. Oletetaan, että seuraava yksinkertainen luokka mallintaa pankkitiliä: class PankkiTili private double saldo=0; // Konstruktori public PankkiTili(double alkusaldo) if( alkusaldo > 0) saldo = alkusaldo; else saldo = 0; // Saantimetodi - saldon antaminen public double annasaldo() return saldo; /* Metodit - tililtä otto ja tilille pano*/ public boolean otatililta(double otto) boolean ottook; if(otto > 0 && otto <= saldo) saldo = saldo - otto; ottook = true; else ottook = false; return ottook; 13

public boolean panetilille(double pano) boolean panook; if(pano > 0) saldo = saldo + pano; panook = true; else panook = false; return panook; Oletetaan, että kaksi säiettä käyttää samaa PankkiTili-oliota yhtä aikaa; toisesta ollaan tekemässä panoa ja toisesta ottoa. Olkoon saldo ennen kumpaakin tapahtumaa 1000; tililtä otetaan 500 ja tillille pannaan 600. Koska säikeet ovat toisistaan riippumattomia, huonolla onnella ne saattavat toimia seuraavasti: Ottoa suorittava säie lukee saldon 1000 1. Panoa suorittava säie lukee saldon 1000 2. Panoa suorittava säie kirjoittaa saldo = 1600 3. Ottoa suorittava säie kirjoittaa saldo = 500 Näin tilin saldoksi jää 500, vaikka pitäisi jäädä 1100. Hyvällä onnella tietysti voisi käydä niinkin, että otto jäisi huomiotta. Tämä ei kuitenkaan ole ohjelmoinnin kannalta toivottava tilanne ja se voidaankin ratkaista kilpailun synkronoinnilla. Koska kaikki Javan oliot voivat toimia monitoreina, jokaisella oliolla Javassa on lukko (lock). Lukko estää suorittamasta useita synkronoituja metodeja yhtä aikaa. Kun säie kutsuu olion synkronoitua metodia, se ottaa haltuunsa olion lukon; tällöin mitkään muut säikeet eivät voi kutsua olion mitään synkronoitua metodia, vaan niiden toiminta estetään, kunnes ensimmäinen säie luopuu lukosta, ts. ensiksi kutsutusta synkronoidusta metodista palataan tai säie luopuu lukosta metodia suorittaessaan. Metodista palaaminen voi tapahtua joko normaalisti tai virheen tai poikkeuksen seurauksena. Tavallisia, ei-synkronoituja metodeja sen sijaan voidaan kutsua samanaikaisesti toisistakin säikeistä. Metodi voidaan määritellä synkronoiduksi lisäämällä sen esittelyyn sana synchronized. Tästä komennosta on toinenkin muoto synchronized(lukkoolio), jolla voidaan lukita jokin muu olio (tässä tapauksessa lukkoolio) kuin metodin omistava olio, näin voidaan lukita myös koodilohko eikä tarvitse rajoittua koko metodiin. 14

Seuraavat määritelmät ovat ekvivalentit: synchronized void f() // Metodin runko void f() synchronized(this) // Metodin runko Yleisin käyttötapa on luonnollisesti lukita this, mutta muitakin lukitusmekanismeja joutuu joskus soveltamaan. Näitä menetelmiä käsitellään jatkossa tarkemmin. Pankkitililuokka saataisiin toimimaan rinnakkaisena seuraavasti: class RinnakkaisPankkiTili private double saldo=0; public RinnakkaisPankkiTili (double alkusaldo) if( alkusaldo > 0) saldo = alkusaldo; else saldo = 0; synchronized public double annasaldo() return saldo; synchronized public boolean otatililta(double otto) boolean ottook; if(otto > 0 && otto <= saldo) saldo = saldo - otto; ottook = true; else ottook = false; return ottook; synchronized public boolean panetilille(double pano) boolean panook; if(pano > 0) saldo = saldo + pano; panook = true; else panook = false; return panook; 15

Nyt yllä esiintynyt datan turmeltuminen ei ole mahdollinen, sillä säie, joka kutsuu otatililtametodia saa haltuunsa olion lukon ja jos toinen säie kutsuu panetilille-metodia, se joutuu estetyksi, kunnes ensimmäinen säie suorittaa oton valmiiksi. Kannattaa huomata, että myös metodi annasaldo on määritelty synkronoiduksi, jotta sitä ei voi kutsua samanaikaisesti tilin saldoa muuttavien metodien kanssa. Luokan konstruktoria ei voi määritellä synkronoiduksi, eikä siihen ole syytäkään, sillä olio luodaan vain yhdessä säikeessä kerran. Mikäli jostain ohjelmointiteknisestä syystä jouduttaisiin käyttämään jo olemassa olevaa luokkaa, jota ei ole kirjoitettu rinnakkaisuutta silmälläpitäen, eikä koodia voisi kopioida sellaisenaan, voitaisiin käyttää hyväksi periytymistä. Kirjoitetaan PankkiTili-luokasta periytyvä luokka, jossa on täsmälleen samat metodit kuin alkuperäisessä luokassa, mutta synkronoituina ja kutsutaan emoluokan metodeja super-viitteellä: class SynkronoituPankkiTili extends PankkiTili public SynkronoituPankkiTili (double alkusaldo) super(alkusaldo); synchronized public double annasaldo() return super.annasaldo(); synchronized public boolean otatililta(double otto) return super.otatililta(otto); synchronized public boolean panetilille(double pano) return super.panetilille(pano); Nyt SynkronoituPankkiTili toimii muuten täsmälleen kuten alkuperäinen PankkiTili, mutta metodit on synkronoitu ja luokkaa voidaan käyttää monisäikeisessä ohjelmassa. Huomautus. Kun luokka peritään, lapsiluokassa uudelleen määriteltävät synkronoidut metodit eivät automaattisesti ole synkronoituja, vaan ne on tarvittaessa määriteltävä sellaisiksi. Kun lapsiluokasta kutsutaan kantaluokan synkronoitua metodia super-viitteen avulla, olio lukkiutuu vaikka lapsiluokan metodia ei olisikaan määritelty synkronoiduksi, koska kantaluokan metodi on synkronoitu. Koska yllä käsitelty lukko on oliokohtainen, oliokohtaisten synkronoitujen metodien kutsuminen ei vaikuta luokkakohtaisiin static-metodeihin. Javassa on lisäksi toteutettu luokkakohtainen lukko, joten myös static-metodit voidaan määritellä synkronoiduiksi. Luokkakohtainen ja oliokohtainen lukko ovat toisistaan riippumattomat, joten kun luokkakohtainen synkronoitu metodi on ajossa, voidaan kutsua oliokohtaista synkronoitua metodia ja päinvastoin. Luokkakohtaista lukkoa käyttäen myös RinnakkaisPankkiTili-luokan olion luominen saataisiin synkronoitua, mikäli se olisi tarpeellista. Nyt voitaisiin nimittäin muuttaa luokan konstruktori private-tyyppiseksi, jolloin olion muodostaminen olisi mahdollista ainoastaan luokan sisältä. Tällöin luokkaan tarvitaan staattinen metodi, joka huolehtii instanssin luomisesta ja tämä voi olla synkronoitu. Näin ollen luokan koodi muuttuisi muotoon: 16

class RinnakkaisPankkiTili private double saldo=0; synchronized static public RinnakkaisPankkiTili luoolio(double alkusaldo) return new RinnakkaisPankkiTili(alkuSaldo); private RinnakkaisPankkiTili (double alkusaldo) // jne Tällöin tiliä käyttävä ohjelma ei voi luoda pankkitilioliota RinnakkaisPankkiTili rpt = new RinnakkaisPankkiTili(123.23); kuten ennen vaan on luotava se seuraavasti: RinnakkaisPankkiTili rpt = RinnakkaisPankkiTili.luoOlio(123.23); Huomautus 2. Synkronointi on resursseja kuluttava toimenpide, joten kaikkia luokkia ja niiden metodeja ei kannata kirjoittaa synkronoiduiksi, vaan huolellisella suunnittelulla pitää synkronoitujen metodien määrä mahdollisimman pienenä mutta kuitenkin riittävänä. 2.3.2. Yhteistoiminnan synkronointi Jokaisella oliolla on monitorina lukon lisäksi odotusjoukko (wait set), johon voidaan vaikuttaa vain Object-luokan metodeilla wait(), notify() ja notifyall() sekä Thread-luokan metodilla interrupt(). Olion odotusjoukko koostuu niistä säikeistä, joiden toiminta on estetty kutsumalla säikeestä olion wait-metodia. Javan virtuaalikone pitää sisäisesti yllä odotusjoukkoa kullekin oliolle eikä päästä säikeitä suoritukseen ennen kuin odotuksen ilmoitetaan loppuvan tai se loppuu jonkin poikkeustoiminnon seurauksena. Jotta säie voisi kutsua olion wait-metodia, sillä on oltava hallussaan olion lukko. Toisin sanoen, metodia voi koodissa kutsua ainoastaan synkronoidussa metodissa tai synkronoidun lohkon sisältä. Sama pätee notify- ja notifyall-metodeihin. Metodista wait on kolme muotoa: wait() o odottaa, kunnes säie vapautetaan wait(millisecs) o odottaa, kunnes säie vapautetaan tai millisecs millisekuntia on kulunut wait(millisecs,nanosecs) o odottaa, kunnes säie vapautetaan tai millisecs millisekuntia ja nanosecs nanosekuntia on kulunut Kun säie siirtyy odotustilaan, se luonnollisesti myös vapauttaa olion lukon. Paitsi ajastettuna, säie voi vapautua odotuksesta, mikäli: Jokin toinen säie kutsuu olion notify()-metodia. Tällöin jokin olion odotusjoukon säikeistä vapautuu. Valinnan tekee järjestelmä satunnaisesti. Jokin toinen säie kutsuu olion notifyall()-metodia. Tällöin kaikki olion odotusjoukon säikeet vapautuvat Jokin toinen säie keskeyttää (interrupt) odottavan säikeen. Tällöin heitetään poikkeus InterruptedException. 17

Kun säie vapautuu, se kilpailee normaaliin tapaan muiden säikeiden kanssa olion synkronoinnissa. Jos säie on keskeytetty, InterruptedException heitetään vasta silloin, kun säikeelle annetaan kontrolli olioon. Esimerkki. Kirjoita ohjelma, jossa omassa säikeessään toimiva tuottajaolio tuottaa kokonaislukuja niin ikään omassa säikeessään toimivalle kuluttajaoliolle. Tuottamiseen kuluu ennalta määrätty aika; samoin kuluttamiseen. Jokainen tuotettu luku on kulutettava täsmälleen kerran. Ratkaisu: Tehtävä voidaan ratkaista niin, että kirjoitetaan luokka, jossa on jäsenmuuttuja tuotetulle kokonaisluvulle ja synkronoidut metodit setvalue(), joka asettaa uuden tuotetun arvon getvalue(), joka palauttaa viimeksi tuotetun luvun Luokan olio toimii monitorina, joka synkronoi lukujen tuottamisen ja kuluttamisen niin, että uutta lukua ei voida tuottaa ennen kuin edellinen on annettu kulutettavaksi eikä myöskään jo kerran kulutettavaksi annettua lukua voida kuluttaa uudelleen. Tämä toteutetaan tutkimalla booleantyyppistä jäsenmuuttujaa joka kertoo, onko tuotettu luku jo kulutettu. Tämän perusteella säie asetetaan odottamaan, ellei operaatiota voida suorittaa. Kun tuottaminen tai kulutettavaksi luovuttaminen on tapahtunut, on odottavalle säikeelle ilmoitettava muutoksesta. Lopullinen ohjelma on // Monitoriolio class ProdConMonitor int myint=0; private boolean valueset = false; synchronized void setvalue(int newval) if(valueset) try this.wait(); catch(interruptedexception ie) myint = newval; System.out.println("Produced " + myint); valueset = true; this.notify(); synchronized int getvalue() if(!valueset) try this.wait(); catch(interruptedexception ie) // Handle Excp valueset = false; System.out.println("Consuming " + myint); this.notify(); return myint; 18

// Tuottaja class IntProducer extends Thread ProdConMonitor mymonitor; public IntProducer(ProdConMonitor pcm) mymonitor = pcm; public void run() int i = 1; while(true) // Produce new Int try Thread.sleep(10); catch(interruptedexception ie) break; mymonitor.setvalue(i); i++; // Kuluttaja class IntConsumer extends Thread ProdConMonitor mymonitor; public IntConsumer(ProdConMonitor pcm) mymonitor = pcm; public void run() while(true) // Consume next int mymonitor.getvalue(); try Thread.sleep(100); catch(interruptedexception ie) break; 19

// Pääohjelma public class NotificationEx public static void main(string[] args) ProdConMonitor pcm = new ProdConMonitor(); IntProducer ipprod = new IntProducer(pcm); IntConsumer iccon = new IntConsumer(pcm); iccon.start(); ipprod.start(); 3. Rinnakkaisen ohjelmoinnin malleja Seuraavaksi esitetään kaksi yleiskäyttöistä rinnakkaisen ohjelmoinnin mallia: Tuottajakuluttajamalli ja Lukija-kirjoittajamalli. Nämä kumpikin esiintyvät rinnakkaisissa ohjelmissa eri muodoissa. 3.1. Tuottaja-kuluttajamalli Useat rinnakkaisen ohjelmoinnin suunnitteluongelmat voidaan ratkaista käyttämällä tuottaja kuluttajamallia. Mallissa on kahdentyyppisiä säikeitä (tai prosesseja): Tuottajat o Luovat uusia olioita (dataa), jotka lähetetään kuluttajille. Kuluttajat o Käsittelevät tuottajien luomia olioita. Malli on yleinen ja soveltuu moniin erilaisiin tapauksiin, kuluttaja voi olla esimerkiksi yksittäinen tulostusprosessi, joka käsittelee useista lähteistä saapuvaa tulostettavaa dataa. Tällöin yhtä kuluttajaa kohti on useita tuottajia. Joskus tarvitaan useita kuluttajia, jos esimerkiksi tuottajat tuottavat monen tyyppisiä olioita, jotka kaikki vaativat omanlaisensa kuluttajan käsittelemään niitä. Yksinkertaisimmillaan mallin toteutus on yhden tuottajan ja yhden kuluttajan tapauksessa, kun operaatio etenee synkronoidusti, ts. tuottaja tuottaa olion, kuluttaja käsittelee sen minkä jälkeen tuottaja tuottaa uuden olion jne. Yhteistoiminnan synkronointia käsittelevässä osassa tehtiin esimerkkinä tällainen toteutus, joka on kuitenkin moniin tapauksiin liian joustamaton; usein on tarpeellista antaa tuottajan luoda useita olioita ja kuluttajan kuluttaa niitä omaan tahtiinsa. Tällöin tarvitaan puskuria sisältämään tuotetut oliot. Mikäli puskuri on ääretön, ei tarvitse huolehtia sen täyttymisestä. Käytännössä ääretön puskuri voidaan toteuttaa linkitetyllä listalla. Javan tapahtumankäsittely käyttää ääretöntä puskuria, ns. tapahtumajonoa (event queue). Kun ohjelmassa tuotetaan jokin UI-tapahtuma, siitä luodaan olio, joka menee tapahtumajonoon. Tapahtumanvälittäjä (event dispatcher) hakee tapahtumaoliot jonosta yksitellen ja välittää ne edelleen asianmukaiseen paikkaan käsiteltäviksi. Monesti on tarkoituksenmukaista käyttää kuitenkin käyttää äärellistä puskuria, jolloin se voidaan toteuttaa taulukkona. Etuna on kirjoituksen ja lukemisen nopeus ja yksinkertaisuus, mutta toisaalta on pidettävä huoli siitä, että täyteen puskuriin ei enää kirjoiteta. Useimmiten puskuri toteutetaan (synkronoituna) syklisenä puskurina (rengaspuskuri, circular buffer), jolloin puskurin viimeisen 20

paikan jälkeen kirjoitettaan aina ensimmäiseen jne. Tässäkin tapauksessa on luonnollisesti pidettävä huoli siitä, että täyteen puskuriin ei enää kirjoiteta, koska silloin tietoa häviää. Kirjoitetaan Java-luokka CircularBuffer, jossa on toteutettu metodit put ja take puskuriin lukemista ja kirjoittamista varten. Metodit on kirjoitettava synkronoiduiksi, jotta puskuria voidaan käyttää useammasta säikeestä. Tällöin luokkaa voidaan käyttää Tuottaja-kuluttajamallin toteutuksessa; tuottajia ja kuluttajia voi olla useita yhtä puskuria kohti. public class CircularBuffer protected final Object[] buffer; protected int putptr = 0; protected int takeptr = 0; protected int queueditems = 0; public CircularBuffer(int size) throws IllegalArgumentException // Check first that size is OK if( size <= 0) throw new IllegalArgumentException(); buffer = new Object[size]; public int getcapacity() return buffer.length; public synchronized int getcurrentsize() return queueditems; public synchronized void put(object obj) throws InterruptedException // Wait while buffer is full while(queueditems == buffer.length) wait(); // Put object in buffer buffer[putptr] = obj; // Increase putptr cyclically and increase current size putptr = (putptr +1) % buffer.length; queueditems++; // Signal if put to empty buffer if(queueditems == 1) notifyall(); 21

public synchronized Object take() throws InterruptedException // Wait while buffer is empty while(queueditems == 0) wait(); Object retobj = buffer[takeptr]; // Increase takeptr cyclically and decrease current size takeptr = (takeptr +1) % buffer.length; queueditems--; // Signal if taken from full buffer if(queueditems == (buffer.length-1) ) notifyall(); return retobj; Huomaa kuitenkin, että mikäli tuottajia ja kuluttajia on suuri joukko, notifyall()-metodia käyttävä toteutus tulee raskaaksi. Tällaisessa tapauksessa voidaan joutua tehokkuussyistä suunnittelemaan erilainen, esimerkiksi ns. kohdistettua ilmoittamista käyttävä toteutus. Nyt voidaan kirjoittaa Producer- ja Consumer-luokat jotka käyttävät hyväkseen ylläolevaa puskuria. Luokat kannattaa kirjoittaa abstrakteiksi, koska yleensä tuottaminen ja kuluttaminen ovat tapauskohtaisia operaatioita ja näin ollen metodit produce ja consume tulee määritellä uudelleen toteutuksessa. abstract class Producer private CircularBuffer cbbuffer; public Producer(CircularBuffer cb) cbbuffer = cb; abstract Object produce(); public void putobject(object obj) throws InterruptedException cbbuffer.put(obj); abstract class Consumer private CircularBuffer cbbuffer; public Consumer(CircularBuffer cb) cbbuffer = cb; abstract void consume(object obj); public Object takeobject() throws InterruptedException return cbbuffer.take(); public int getnumstored() return cbbuffer.getcurrentsize(); 22

Esimerkki. Nukkuvan parturin ongelma. Ongelma on peräisin Dijkstran semaforien käyttöä koskevasta artikkelista ([Dij2]). Parturilla on ateljeessaan leikkuutuolinsa lisäksi neljä tuolia odottaville asiakkaille. Kun parturi on leikannut asiakkaan hiukset, hän ottaa käsittelyyn ensimmäiseksi odottamaan tulleen asiakkaan. Mikäli asiakkaita ei ole, parturi nukahtaa leikkuutuoliinsa. Jos kaikki odotussalin tuolit ovat täynnä, sisään pyrkivä asiakas kääntyy pois ja tulee myöhemmin uudelleen. Kirjoita ohjelma, joka simuloi tätä tilannetta. Edellä konstruoituja luokkia käyttäen nukkuvan parturin ongelma voitaisiin ratkaista seuraavalla koodilla. Ohjelmaan on toteutettu graafinen käyttöliittymä ilmoittamaan tapahtumista. Tähän liittyvä koodi on kommentoitu pois. import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; class Customer private int custno; public Customer(int no) custno = no; public String tostring() return "Customer number " + custno; class Barber extends Consumer implements Runnable private String name = "Monitor Barber"; private int maxcuttime; private JTextArea noteboard; private Random rndgen; public Barber(CircularBuffer cb,int maxtime, JTextArea ta) super(cb); maxcuttime = maxtime; noteboard = ta; rndgen = new Random(); protected void consume(object obj) Customer c = (Customer)obj; // Ilmoita leikkauksen alkavan try Thread.sleep( 1000 + Math.abs(rndGen.nextInt(1000 * maxcuttime)) ); catch(interruptedexception ie) Thread.currentThread().interrupt(); // Ilmoita leikkauksen loppuneen 23

public void run() Object obj; while(true) try if( getnumstored() == 0 ) // Ilmoita parturin nukahtaneen obj = takeobject(); consume(obj); catch(interruptedexception ie) break; class CustomerAppearance extends Producer implements Runnable private int maxappeartime; private JTextArea noteboard; private int CustomerNum = 0; private Random rndgen; public CustomerAppearance(CircularBuffer cb,int maxtime,jtextarea ta) super(cb); maxappeartime = maxtime; noteboard = ta; rndgen = new Random(); protected Object produce() Object obj; try Thread.sleep(1000 + Math.abs(rndGen.nextInt(1000 * maxappeartime)) ); catch(interruptedexception ie) ; CustomerNum++; obj = new Customer(CustomerNum); return obj; public void run() Customer c; while(true) try c = (Customer)produce(); putobject(c); 24

// Ilmoita asiakkaan saapuneen catch(interruptedexception ie) break; public class BarberShop extends JFrame implements WindowListener,ActionListener CustomerAppearance caqueue = null; Barber thebarber; Thread BarberThread; Thread CustomerThread; JButton startbutton; JTextArea threadnote = null; CircularBuffer chairs = null; public BarberShop() // Luodaan ikkunan käyttöliittymä // Luodaan puskuri ja säikeet try chairs = new CircularBuffer(4); catch(illegalargumentexception iae) System.out.println("Invalid constrution of circular buffer "); iae.printstacktrace(); System.exit(-1); caqueue = new CustomerAppearance(chairs,6,threadNote); thebarber = new Barber(chairs,2,threadNote); BarberThread = new Thread(theBarber); CustomerThread = new Thread(caQueue); public static void main(string [] argv ) // Käynnistetään ohhjelma java.awt.eventqueue.invokelater(new Runnable() public void run() BarberShop bsprog = new BarberShop(); bsprog.setvisible(true); bsprog.addwindowlistener(bsprog); ); // Start client producer bsprog.customerthread.start(); // Parturin käynnistäminen ja keskeyttäminen napista 25

public void actionperformed(actionevent ae) if( ae.getsource() == startbutton) if(barberthread.isalive()) BarberThread.interrupt(); else BarberThread.start(); Toteutuksessa luokka CustomerAppearance toimii tuottajana ja perii abstraktin luokan Producer. Luokka Barber taas toimii kuluttajana ja perii abstraktin luokan Consumer. Ne kommunikoivat yhteisen CircularBuffer-olion välityksellä. Tämä syklinen puskuri sisältää neljä paikkaa ja vastaa odotushuoneen tuoleja. Parturi-olio kuluttaa yhden asiakkaan satunnaisessa ajassa jonka yläraja annetaan sekunteina olion konstruktorin yhteydessä (yllä 2 sekuntia). Tämä on toteutettu implementoimalla consume()-metodi. Asiakkaita saapuu satunnaisin väliajoin, väliajan yläraja annetaan CustomerAppearance-olion luomisen yhteydessä (yllä 6 sekuntia), tämän toteutus on luokan produce() -metodissa. Asiakkaita alkaa saapua heti ohjelman käynnistyessä ja parturi voidaan käynnistää painonapista; samasta napista parturin toiminta lopetetaan. Käyttöliittymässä olevaan tekstialueeseen tulostetaan asiakkaan saapuminen, asiakkaan leikkauksen alkaminen ja päättyminen. Samoin parturin nukahtaminen, mikäli asiakkaat loppuvat. Olennaisia lauseita ohjelmassa ovat c = (Customer)produce(); putobject(c); joilla tuotetaan uusi asiakasolio ja syötetään se sykliseen puskuriin, sekä obj = takeobject(); consume(obj); joka lukee puskurista seuraavan asiakasolion. Huomaa, että olio joudutaan tyypittämään consume() -metodissa Customer-tyyppiseksi, koska syklinen puskuri on kirjoitettu yleisesti Object-tyyppisille olioille, jolloin siihen voidaan kirjoittaa mitä tahansa olioita. Koska Java ei tue moniperintää, ja luokkien Barber ja CustomerAppearance on perittävä Consumerja Producer-luokat, saadaan Barber ja CustomerAppearance-oliot toimimaan omissa säikeissään antamalla niiden toteuttaa Runnable-rajapinta. Tällöin niihin voidaan kirjoittaa run-metodi, ja käynnistää se omassa säikeessään, kuten ohjelmassa on tehty. Asiakkaiden tuottaminen tapahtuu nyt määrittelemällä uudelleen Producer-luokan abstrakti metodi produce() ja kuluttaminen määrittelemällä uudelleen Consumer-luokan abstrakti metodi consume(). 26