Trådar i Java

Vi är alla bekanta med att datorer kan göra flera saker “samtidigt”. Vi kan kompilera ett program samtidigt som vi använder en texteditor och spelar en musikfil. I de flesta fall så åstadkommes “samtidigheten” genom att operativsystemet låter processorn skifta mellan de olika uppgifterna genom att exekvera var och en av dem en bråkdel av en sekund i taget i och sedan fortsätta till nästa. På detta sätt får användaren en illusion av att processorn utför de olika uppgifterna parallellt.

Också ett Java-program kan hålla på med flera saker “samtidigt”; i detta fall säger man att programmet innehåller flera trådar. För spel som de vi betraktar i denna laboration kommer vi att behöva ha flera trådar i gång.

Programmet behöver dels ständigt lyssna på användarens tangenttryckningar och vidarebefordra denna information till modellen, dels ständigt uppdatera spelet även om användaren inte gör något. Om vi kan beskriva dessa två aktiviteter oberoende av varandra blir programstrukturen enklare. Helt oberoende blir de förstås inte; kommandon från lyssnandet måste göras tillgängliga för den tråd som uppdaterar spelets tillstånd.

Under exekvering kommer vi alltså att ha en tråd som ansvarar för den pågående spelinstansen. Denna tråd måste ha tillgång till en heltalsvariabel command, representerande det aktuella kommandot, en referens model till en spelmodell och en referens view till en spelvy. Tråden exekverar en ständig loop

while(isRunning) {
  gameModel.gameUpdate(popLastKey());
  view.repaint();
  Thread.sleep(updateInterval);
}

I metoden startGame(…) i GameController skapas en tråd mha av följande kod:

  gameThread = new Thread(this);
  gameThread.start();

Då kommer en helt ny tråd att börja exekvera koden i metoden run(), där ovanstående loop återfinns. Just detta kan vara svårt att förstå: Den ursprungliga tråden (från main) anropar gameThread.start(). Detta anrop tar slut omedelbart (så att den ursprungliga tråden kan fortsätta med sina aktiviteter), men dessutom startas en parallel tråd som exekverar run(). Java ser sedan till att dessa två trådar får dela processorns tid.

Vi har alltså två trådar: den ursprungliga, som lyssnar på tangentbordstryckningar och den nya som uppdaterar spelet. Dessa kommunicerar genom att klassen GameController får den senaste tangentryckningen sparad i lastKey. Denna används sedan av speltråden för att skicka till själva spelet.

Uppdateringstråden terminerar då spelet tar slut.

Kommunikation och synkronisering leder till nya och svåra problem vid programmering, beroende på att då en tråd uppdaterar gemensamma data så är detta oftast något som kräver flera instruktioner; Java-satsen

x = 7 * x;

översätts till något i stil med

  1. lägg värdet i cellen x på stacken
  2. lägg 7 på stacken
  3. ersätt de två översta elementen i stacken med deras produkt
  4. lagra värdet överst på stacken i cellen x

Problemet är att en uppdaterande tråd kan bli avbruten då den hunnit delvis genom denna följd operationer. Antag att x lagrar värdet 10 och att en tråd börjar med ovanstående följd men blir avbruten efter steg 2. Denna tråd har alltså 7 och 10 överst i sin stack. Om en annan tråd sedan (utan avbrott) gör samma uppdatering så får x värdet 70. När den första tråden senare får fortsätta så multiplicerar den 7 och 10 och lagrar resultatet i x. Effekten av en av de två uppdateringarna har försvunnit! Dessa problem och hur de kan lösas diskuteras i detalj i kursen Parallellprogrammering. Här noterar vi bara att man kan markera en metod i Java som synchronized; detta får effekten att aldrig mer än en tråd kan ha exekvering av denna metod pågående (där exekveringen anses pågående också då tråden avbrutits för att lämna processortid åt en annan tråd). I GameController återfinner vi också

public synchronized void setLastKey(int key) {
  lastKey = key;
}

public synchronized int popLastKey() {
  int key = lastKey;
  lastKey = 0;
  return key;
}

Om man inte hade haft synchronized kring dessa metoder, skulle man kunna missa en tangentryckning.

Om man blir tvungen att programmera med trådar så bör alltså alla metoder som berör delade variabler markeras som synkroniserade. I detta fall är den uppdaterande metoden en enda tilldelning av ett värde till en heltalsvariabel. Detta är något som sker i en instruktion, så just i detta fall skulle inga problem uppstå om vi uteslöt synkroniseringen, men vi tar det säkra före det osäkra.

Menu