Metoder för att förhindra kapplöpning

Kapplöpning innebär att flera trådar samtidigt arbetar med samma data, varvid resultatet kan bli felaktigt på grund av att trådarna inte är överens om datats aktuella värde. Det här avsnittet handlar om att förhindra kapplöpning genom att se till att datat aldrig används av mer än en tråd åt gången.

Litteratur

Lea: Concurrent programming in Java avsnitt 2.1 - 2.3

Se till att data aldrig kan ändras över huvud taget (2.1)

Om ingenting någonsin kan ändra värde är det ingen risk att olika trådar ser olika värden. Det kommer aldrig någonsin att förekomma mer än ett enda värde i någon tråd.

tillståndslösa objekt

Ett objekts tillstånd är dess fullständiga representation, dvs i första hand värdet av alla dess fält men även värdet av alla fält i alla objekt det är beroende av, innehållet i filer som beskriver det osv.

Ett tillståndslöst objekt har ingen som helst representation, det innehåller till exempel inga fält. Här kommer ett exempel:
class StatelessAdder {

    public int add(int a, int b) {
        return a + b;
    }

}
Det finns ingen risk med att flera trådar använder denna metod samtidigt. Alla trådar kommer att använda sina egna värden på inparametrarna (a och b) och få sin egen summa tillbaks.

objekt med konstant tillstånd

Även objekt vars tillstånd är detsamma under hela dess livstid kan utan problem användas av flera trådar samtidigt. Alla trådar kommar alltid att använda det enda värdet som finns. Här kommer ett exempel:
class ImmutableAdder {
    private final int offset;

    public ImmutableAdder(int a) {
        offset = a;
    }

    public int addOffset(int b) {
        return offset + b;
    }

}
Objektets tillstånd beskrivs av fältet offset, vilket aldrig kan ändra värde eftersom det är konstant (deklarerat som final int).

Observera dock att final inte nödvändigtvis innebär att ett fält är konstant. Om en referens till ett annat objekt är final innebär det bara att fältet alltid kommer att peka på samma objekt, det finns ingenting som förhindrar att det refererade objektet ändrar tillstånd. Antag att ett objekt representerar ett fönster på skärmen och att det har en final referens till ett annat objekt som innehåller förnstrets position på skärmen. Fönstrets position är i högsta grad en del av dess tillstånd och det är i detta fall inte alls säkert att låta flera trådar samtigt manipulera positionsobjektet trots att dess referens är final.

Det går heller inte att låta flera trådar samtidigt skriva till en double eller long även om den är final. Det beror på att skrivning av dessa typer sker i två steg, först ena halvan och sedan den andra. Om en tråd skriver ena halvan av värdet och sedan blir avbruten av en annan tråd som skriver den andra halvan kommer variabeln att innehålla hälften av två olika värden. Lösningen är att deklarera den volatile. I så fall sker alla operationer på variabeln atomärt, dvs de kan ej bli avbrutna.

Exempel på objekt med konstanta tillstånd är abstrakta datatyper som till exempel java.lang.Integer (representerar en int), java.lang.String (representerar en sträng) och java.awt.Color (representerar en färg).

konstruktorer i tillståndslösa objekt

Det viktiga är att det inte går att komma åt ett final fält innan det har fått sitt värde. Om fältet får sitt värde i konstruktorn (som i klassen ImmutableAdder ovan) får konstruktorn till exempel inte lämna ut referensen this. Den får heller inte anropa andra metoder med this som parameter. Det går för övrigt inte att synkronisera en konstruktor.

Se till att data endast kan ändras av en tråd i taget (2.2)

Lite mer om synchronized och volatile

I avsnittet om Javas tråd-API finns en genomgång av synchronized. Det som förtjänar att ytterligare poängteras är att det finns ett lås per objekt. Det går alltså inte att låsa primitiva typer som int, float osv. Däremot går det utmärkt att låsa arrayer eftersom de ärver av Object och därmed är en sorts objekt.

Det som låses är alltså accessen till just det objekt som anges vid synchronized-satsen. Om en metod eller ett fält ligger i samma klass som synchronized-satsen eller i en superklass spelar ingen roll, det är objektet som låses. Däremot ligger inte statiska metoder och fält i något objekt och tillträdet till dem påverkas inte av att ett objekt av klassen låses. Statiska metoder låses genom att utnyttja låset i ett objekt av klassen Class. Varje klass i Java representeras automatiskt av ett objekt av klassen Class. Detta objekt kan refereras som MinLillaKlass.class. De statiska medlemmarna i en klass kan alltså låsas genom att skriva till exempel:
synchronized ( MinLillaKlass.class ) {
    ...
}
eller vid deklarationen av en statisk metod:
public static synchronized void enMetod() {
    ...
}

Ordet synchronized är inte en del av en metods signatur. En omdefinierad metod i en subklass blir alltså inte synchronized bara för att metoden i superklassen är det.

Synchronized och volatile påverkar även ett fälts synlighet. Värden som skrivs i ett synchronized-block eller till ett fält som är volatile måste vara synliga för andra trådar som går in i synchhronized-block eller använder samma volatile fält. Observera att om vi inte använder sycnhronized eller volatile finns det inga garantier om synlighet.

Fullständigt synkroniserade objekt

Om vi ska förhindra kapplöpning med hjälp av lås uppstår den svåra frågan "vad och när ska vi låsa?". Mer avancerade svar på den finns i avsnittet designstrategier, här nöjer vi oss med att titta på fullständigt synkroniserade objekt.

I ett fullständigt synkroniserat objekt (en komplett definition finns på sid 78) är alla metoder synchronized. Det får heller inte finnas några fält som är deklarerade public, därför att de skulle kunna ändras utan att gå in i någon av synkroniserade metoderna. Ett fullständigt synkroniserat objekt är garanterat trådsäkert, det finns aldrig någon risk för kapplöpning. Det är heller aldrig några problem att lägga till nya metoder. Så länge de är synchronized förblir objektet trådsäkert. Tyvärr blir ett sådant objekt långsamt att använda eftersom det tar mycket tid hantera låsen.

Singletons

En singleton är en klass som konstruktören inte har avsett att det ska skapas mer än ett objekt av. Ett exempel är java.awt.Toolkit som utgör ett gränssnitt mellan övriga klasser i java.awt och operativsystemets fönsterhantering. För att förhindra att det skapas extra objekt av en singleton måste dess konstruktor vara private. Att en klass är en singleton har inget att göra med om den är trådsäker eller har några synchronized-block. Här ska vi dock titta på några detaljer i en fullständigt synkroniserad singleton.

Här är ett exempel, LazySingletonCounter.java. Metoden instance() ser inte ut som på sid 85 i boken men funktionaliteten är exakt densamma. Observera först att all låsning sker med hjälp av objektet LazySingletonCounter.class. Det är för att det inte ska gå att samtidigt exekvera metoderna next() eller reset() som tillhör det enda objektet och metoden instance() som tillhör klassen. Tillvägagångssättet att inte initiera fältet s förrän det verkligen behövs, dvs första gången instance() anropas, kallas lazy initialization.

En intressant variant, StaticCounter.java, finns på sid 86. Här är alla fält och metoder statiska och konstruktorn private. Det är inte meningen att det någonsin ska skapas något objekt av klassen. Ett annat exempel på en sådan klass är java.lang.Math som innehåller metoder för matematiska beräkningar. Synkroniseringen blir väsentligt enklare än i det förgående fallet.

Se till att data endast kan nås av en tråd (2.3)

Om vi kan försäkra oss om att det är omöjligt för mer än en tråd åt gången att använda ett objekt finns det inget behov av att synkronisera något i det objektet. För att åstadkomma detta måste vi undvika nedanstående fyra sätt på vilka en referens, r, till något objekt kan komma att användas utanför en metod, m, och därmed användas av någon tråd vi inte har kontroll över:
  1. m skickar r som inparameter till någon annan metod eller konstruktor
  2. m returnerar r
  3. m skriver r i till exempel ett fält som kan nås av andra trådar
  4. m släpper (på något av ovanstående tre sätt) ut en referens till något annat objekt som i sin tur innehåller r.

Objekt är lokala variabler

Om ett objekt är en lokal variabel i en metod och referensen till det aldrig slipper ut finns det ingen risk för kapplöpning. Varje gång metoden anropas kommer ett nytt objekt att skapas som endast används i just det anropet (av just den tråden). Det är till och med OK att lämna ut objektet om det är det sista som sker i metoden:
class Plotter {                    // fragments
    // ...

    public void showNextPoint() {

        Point p = new Point();
        p.x = computeX();
        p.y = computeY();
        display(p);
    }

    protected void display(Point p) {
        // somehow arrange to show p.
    }

}
I metoden showNextPoint() ovan lämnas objektet p ut när display(p) anropas. Det är dock ofarligt eftersom det aldrig mer används av showNextPoint(). Om det används av någon annan tråd senare kommer den att vara ensam om det och kapplöpning förekommer därför ändå inte. Skulle det däremot vara nödvändigt att lämna ut objektet innan showNextPoint() är slut, som i fallet nedan, finns det ändå åtgärder för att slippa synkronisera.
    public void showNextPoint() {

        Point p = new Point();
        p.x = computeX();
        p.y = computeY();
        doSometingWithP(p);  /* This line is new. */
        display(p);
    }
Vi kan lösa problemet med någon av de åtgärder som beskrivs på sid 102. Till exempel kan vi skicka en kopia av objektet om det som i detta fall inte är själva objektet som är intressant utan bara värdet av dess fält. I sådana fall byter vi raden doSometingWithP(p) ovan mot doSometingWithP(new Point(p.x, p.y)).

Objekt är unika för varje tråd

Om endast en tråd kan referera ett objekt finns det ingen risk för kapplöpning. Denna metod är lite svår att tillämpa eftersom det inte är helt lätt att inse vilket objekt som hör till vilken tråd. Det finns två sätt att göra ett objekt lokalt för en tråd:
  1. Låt objektet vara ett fält i en subklass till Thread. Det är dock inte så enkelt som det låter eftersom det är bara run(), inte hela Thread-objektet, som utgör själva tråden. Det går alldeles utmärkt för andra trådar än den som körs i ett Thread-objekts run() att komma åt Thread-objektets metoder och fält. För att vara säker på att alltid använda det egna Thread-objektets fält måste det accessas via metoden Thread.currentThread(), vilken returnerar en referens till det exekverande trådens Thread-objekt. Om vi till exempel har deklarerat följande subklass till Thread:

  2. public class MyOwnThread extends Thread {
        private Vector v = new Vector();
    måste objektet v refereras som Thread.currentThread().v
  3. Använd ThreadLocal. Det är en klass som har två metoder,  set() och get(), vilka sparar respektive returnerar ett Object. Poängen är att klassen automatiskt hanterar olika objekt för varje tråd. Om en viss tråd sparar ett objekt met set() får den alltid tillbaks det igen med get(), även om andra trådar gjort set() imellan.

Objekt ägs av ett överordnat objekt

Om det inte går att låta ett objekt stanna inom en metod eller en tråd blir det nödvändigt att låsa med synchronized (kallas dynamisk låsning eftersom låsningen sker när programmet körs). Men om vi kan konstruera programmet så att ett visst objekt (låt oss kalla det part) endast används av ett annat objekt (vi kallar det host) finns det inget behov att synkronisera någonting i part. Om vi genom lämpliga synkroniseringar i host förhindrar att mer en än tråd åt gången kan komma åt referenser till part kan vi vara helt säkra på att det aldrig är mer än en tråd åt gången som arbetar med part, därmed finns ingen risk för kapplöpning. Detta illustreras av bilden på sid 107. För att vara helt säkra på att det inte finns referenser till part utanför host är det mycket lämpligt att part konstrueras av host. Detta att ett objekt under hela sin livstid ägs av ett annat kallas aggregering (aggregation).

Om synkronisering ska implementeras i ett helt osynkroniserat objekt kan det lösas genom att, enligt ovan, låta det osynkroniserade objektet helt ägas av ett annat objekt där vi inför synkroniseringen. Det synkroniserade objektet blir då en adapter. Här kommer ett exempel där den osynkroniserade klassen UnsynchedPoint ägs av adaptern SynchedPoint:
class UnsynchedPoint {
    public double x;
    public double y;
}

class SynchedPoint {
    protected final UnsynchedPoint delegate = new UnsynchedPoint();

    public synchronized double getX() { return delegate.x;}
    public synchronized double getY() { return delegate.y; }
    public synchronized void setX(double v) { delegate.x = v; }
    public synchronized void setY(double v) { delegate.y = v; }

}

Här kommer ett klassdiagram som beskriver ovanstående kod. Den lilla "diamanten" på pilen betyder aggregering (se ovan).

En anledning till att använda en adapter på detta sätt kan vara att vi har behov av både en osynkroniserad Point och en synkroniserad. Med adaptern slipper vi kopiera all kod i Point till SynchedPoint.