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

  1. join()

  2. Anropas t.join() kommer den anropande tråden att sova (låta bli att exekvera) tills tråden t dött.
  3. getName(), setName( String name)

  4. 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.
  5. setDeamon( boolean on)

  6. 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.
  7. sleep(long millis)

  8. Den statiska metoden sleep(long millis) gör att den exekverande tråden sover angivet antal millisekunder.
  9. interrupt()

  10. 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.
  11. currentThread()

  12. Denna statiska metod returnerar en referens till den exekverande trådens Thread-objekt.
  13. yield()

  14. 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:
  1. Tråd 1 kommer in i metoden addToValue() och läser värdet av value.
  2. Tråd 1 blir avbruten (Javamotorn tillämpar ofrivilligt trådbyte).
  3. 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.
  4. 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.