Javas tråd-API
Hur trådar hanteras i ett Javaprogram.
Litteratur
En förträfflig genomgång av Javas tråd-API finns
i Java Tutorial, se http://java.sun.com/docs/books/tutorial/essential/threads/index.html.
Avsnittet Grouping Threads kan du hoppa över.
I kursboken från Javakursen i period 1 (Eriksson: Programutveckling
med Java) behandlas trådhantering i kapitel 7. Det är tyvärr
inte helt aktuellt, det som står om metoderna stop(), suspend()
och resume() stämmer inte.
Avsnitt 1.1.2 i kursboken (Lea: Concurrent Programming in Java) handlar
om detta avsnitt. Lite svårläst som första text om trådar
men bra därför att vi redan här får lite designtips.
Starta en tråd
Hur en tråd startas beskrevs i förra
avsnittet.
Stoppa en tråd
En tråd avslutas (dör) när run() tar slut.
Det kan ske genom att exekveringen når slutparantesen '}',
genom instruktionen return eller genom att ett undantag kastas
och inte fångas. Ofta stöter man på ordet cancellation
vilket innebär att tvinga en tråd att avsluta exekveringen. Hur
detta görs återkommer vi till längre fram i kursen.
Prioroteter
Trådar kan ges olika prioriteter. Syftet är att vi ska kunna
styra operativsystemet till att exekvera en tråd oftare genom att ge
den en högre proritet eller mer sällan genom att ge den en lägre
proritet. I Java kan en tråd ha proriteter från 1 till 10, där
10 är högsta priroteten. Proritet hanteras av metoderna setPriority(int
newPriority) och getPriority() i klassen Thread.
Nu är det inte riktigt så kul i verkligheten. Varken Java Language Specification eller Java Virtual Machine Specification ger något
som helst löfte om hur prioriteter hanteras. Faktum är att en
javamotor (JVM) inte är tvungen att hantera prioriteter över huvud
taget. Ska det vara Write Once Run Anywhere (och det ska det...) är
det därför klokt att skriva programmet så det funkar även
om prioriteterna inte hanteras rätt. Det kan till exempel vara bra att
bara använda nivåerna Thread.MIN_PRIORITY (1), Thread.MAX_PRIORITY
(10) och Thread.NORM_PRIORITY (5) även om det inte finns någon
garanti att ens de fungerar korrekt.
Några övriga metoder för trådkontroll
- join()
Anropas t.join() kommer den anropande tråden att sova (låta
bli att exekvera) tills tråden t dött. - getName(),
setName( String name)
Med hjälp av dessa metoder kan en tråd få ett namn. Det
är användbart till exempel för att debugga program. Genom att
skriva ut trådens namn kan man se vilken tråd urskriften kommer
från. - setDeamon( boolean on)
Om en tråd är en demon kommer javamotorn att avslutas
även om tråden fortfarande lever. Annars fortsätter javamotorn
tills alla trådar dött. - sleep(long millis)
Den statiska metoden sleep(long millis) gör att den exekverande
tråden sover angivet antal millisekunder. - interrupt()
Anropas t.interrupt() sätts en flagga i t. Den kan
läsas med metoden isInterrupted(). Om t sover på grund
av ett anrop till join(), sleep() eller wait()
(se nedan) kastat ett undantag. - currentThread()
Denna statiska metod returnerar en referens till den exekverande trådens
Thread-objekt. - yield()
Denna statiska metod gör att den exekverande tråden avbryts och
någon annan tråd får tillfälle att exekvera. Liksom
för prioriteter (se ovan) finns inga garantier om att denna metod fungerar.
Synkronisering
Betrakta följande program, Racer.java.
Det startar ett antal trådar som alla först adderar 10 till en
och samma variabel ett antal gånger och sedan drar ifrån 10 lika
många gånger. Variabelns värde skrivs ut innan någon
av trådarna startat och när alla trådar exekverat färdigt.
Variabelns värde borde alltid vara det samma båda gångerna,
men i verkligheten visar det sig att värdet efter att trådarna
exekverat är helt slumpmässigt. Varför? Jo, därför
att till exempel följande scenario inträffat:
- Tråd 1 kommer in i metoden addToValue() och läser
värdet av value.
- Tråd 1 blir avbruten (Javamotorn tillämpar ofrivilligt
trådbyte).
- Tråd 2 börjar exekvera, kommer in i addToValue(),
läser samma värde som tråd 1, utför additionen och summan
i value.
- Tråd 1 får exekvera igen, utför additionen och skriver
summan i value.
Resultatet av tråd 2:s addition är nu förlorat. Vid närmare
eftertanke visar det sig att det finns hur många felaktiga scenarion
som helst, alla på grund av att en tråd blir avbruten i addToValue().
Denna typ av problem kallas kapplöpning (race condition).
En beräkning som inte får bli avbruten, som till exempel den
som utförs i addToValue(), kallas kritisk sektion. Lösningen
är att låta trådarna låsa addToValue() genom
att deklarera den synchronized, SynchRacer.java.
Nu fungerar programmet!
Vad är ett lås?
Varje objekt i Java har ett lås. Låset består av en "upptagetflagga"
och en kö (enter queue). Om en metod är synchronized
kommer en tråd som vill in i metoden att kolla om den är upptagen.
Är den det inte sätter tråden upptagetflaggan och går
in i metoden. Är den däremot redan upptagen lägger sig tråden
i kön och sover. När metoden blir ledig kommer javamotorn att
väcka någon tråd i kön och släppa in den i metoden.
Det finns alltså ett lås per objekt. Det betyder att
endast en synkroniserad metod per objekt kan exekveras åt gången.
Observera dock att inget hindrar trådar från att gå in i
osynkroniserade metoder även om objektets lås är låst.
Det objekt som låses är det som tråden har anropat. I programexemplet
ovan är anropet skrivet som um.addToValue( 10 ). Det är
alltså objektet som refereras av um som låses.
Det går även att explicit ange vilket objekt som ska låsas.
Det görs på följande sätt:
synchronized ( obj ) {
...
}
När exekveringen sker inom måsvingarna är objektet obj
låst.
Tyvärr tar det väldigt lång tid att låsa och låsa
upp objekt. Det tar även lång tid att byta exekverande tråd.
Därför är det viktigt att inte låsa i onödan.
Händelsesynkronisering
Här kommer ett program till som inte fungerar, ErrorMailbox.java. Det gick inte alls, producenten
lägger i en massa meddelanden som alla skriver över föregående
meddelande. Därefter hämtar konsumenten samma meddelande gång
på gång. Brevlådan måste göras om så att
läsning och skrivning sker varannan gång. Här kommer en korrekt
brevlåda, Mailbox.java. Nu funkar det! Hemligheten
var metoderna wait() och notifyAll(). Dessa metoder finns
i klassen Object (som alla klasser ärver av). När en tråd
anropar wait() låser den upp det objekt den anropar wait()
på och lägger sig sedan och sover, dock inte i enter queue (se
ovan) utan i en annan kö (wait queue). När en tråd
anropar NotifyAll() flyttas alla trådar i wait queue till enter
queue. Därefter exekverar den tråd som anropade notifyAll()
vidare tills den lämnar synchronized-blocket.
Anledningen till att anropen av wait() finns i en while-loop
och inte i en if-sats är att vi aldrig kan vara säkra
på varför en tråd har vaknat. Det är fullt möjligt
att tråden vaknat av misstag utan att någon anropat notifyAll()
(kallas spurious wakeup). Det kunde också varit så att
vi hade fler trådar som anropade setMail() och getMail().
Vi skulle i sådana fall inte veta om en tråd väckts till
följd av ett anrop av notifyAll() i setMail() eller getMail().
Det går alltså aldrig att vara säker på att ett villkor
är sant bara för att en tråd har anropat wait()
och sedan blivit väckt igen.
Det finns även en metod notify() som bara flyttar en tråd
ur wait queue (inte nödvändigtvis den första) till enter
queue. Använder vi den slipper vi den tid det tar att flytta alla andra
trådar i wait queue. Å andra sidan är risken stor att fel
tråd väcks och fastnar på wait() igen medans den
tråd vi ville väcka förblir sovande i wait queue.
En tråd som anropar wait(), notify() eller notifyAll()
på ett objekt måste ha låst det objektet (kallas att äga
objektets lås).
Förbjudna metoder
Det finns tre metoder i klassen Thread som inte ska användas.
Metoden stop() (som dödar en tråd på momangen)
ska inte användas därför att det är omöjligt att
vara säker på att tråden som dör inte lämnar något
objekt i ett korrupt tillstånd. Om till exempel tråden som dör
är inne i en kritisk sektion kommer den kririska sektionen att låsas
upp när den bara är delvis exekverad, det vill säga i ett tillstånd
när vi absolut inte vill att någon annan tråd ska komma
in där.
Metoderna suspend() och resume() (som får en tråd
att sova respektive vakna igen) ska inte användas därför att
om tråden håller några lås när den somnar kan
ingen annan tråd komma in i det låsta objektet förrän
den sovande tråden väcks igen och exekverar tills den låser
upp det låsta objektet. Detta kan leda till att trådar får
vänta onödigt länge på låset. Om det rent av
är den tråd som ska anropa resume() som får vänta
på låset kommer ingen av dem någonsin att kunna exekvera
igen eftersom de väntar på varandra (kallas deadlock eller
dödläge, mycket mer om det senare i kursen).
En utförlig redogörelse för dessa problem finns på
http://jsp2.java.sun.com/j2se/1.3/docs/guide/misc/threadPrimitiveDeprecation.html.
Trådens livscykel
Denna bild visar vilka olika tillstånd en tråd kan befinna sig
i. Själva tråden skapas inte när Thread-objektet
skapas utan först när metoden start() anropas. Det är
först då trådens minnesarea görs i ordning och placeras
i en kö. Tråden blir då runnable, dvs den placeras
i kön av trådar som bara väntar på processortid (ready
queue). I sinom tid kommer tråden att få exekvera, den är
då running. Därefter placeras den åter i ready queue.
En tråd som varken exekverar eller väntar i ready queue är
not runnable. Det kan den bli till genom att vänta på
att få komma in i ett synchronized-block eller genom att anropa
wait() eller sleep(). Tråden blir återigen runnable
till följd av till exempel notify(), notifyAll(),
att synchronized-blocket blir ledigt eller att den har sovit den
tid som angavs med sleep(). När run() i sinom tid
har exekverats till slut dör tråden och är (hör och
häpna) dead. Den kan då aldrig mer väckas till liv,
det går till exempel inte att anropa start() på den
igen.