Design av synkronisering

Hanteringen av lås kan göra program onödigt långsamma, dels därför att trådar kan förlora tid genom att vänta på lås och dels därför att själva hanteringen av låsen är långsamma operationer. Exempelvis kan problem uppstå genom att onödigt stora kodblock synkroniseras fast delar av dem skulle kunna utföras parallellt eller genom att ett och samma lås används för att synkronisera flera olika kodblock som egentligen är oberoende av varandra. I avsikt att motverka dessa problem kan resultatet bli att onödigt många lås införs varvid programmet måste ägna onödigt mycket tid åt att låsa och låsa upp. Det här avsnittet handlar om några listiga metoder att förhindra kapplöpning utan att programmet blir onödigt långsamt.

Litteratur

Lea: Concurrent Programming in Java, avsnitt 2.4

Refactoring

Uttrycket refactoring används flitigt i boken. Det betyder att ändra i programmet utan att ändra dess funktionalitet. Syftet kan vara att koden ska bli mer lättförståelig eller att programmet ska bli snabbare.

En enkel regel

Utan att behöva kasta sig in i alltför listiga strategier räcker följande enkla direktiv väldigt långt: Nu kastar vi oss in i de listiga strategierna.

Krympa synchronized-block (2.4.1)

Accessorer (get()-metoder)

Metoder som bara returnerar värdet på ett fält behöver inte nödvändigtvis vara synkroniserade även om andra trådar kan tänkas skriva till fältet. Det är två saker man måste ha i tankarna om accessorer inte är synkroniserade:
  1. Fälten får aldrig anta några otillåtna värden. Det måste gå att när som helst avbryta en tråd som utför beräkningar med ett fält och låta en annan tråd returnera fältets värde. Detta innebär att fält av typen double och long måste vara volatile. Om fältet är en objektreferens räcker det inte med att själva referensen har ett tillåtet värde, även det refererade objektets fält måste ha det.
  2. Alla gjorda uppdateringar av fälten kanske inte syns. Förutom att garantera exklusiv access garanterar synchronized och volatile också att uppdateringar gjorda av andra trådar är synliga. Om accessorerna inte är synkroniserade kan det hända att ett värde som en annan tråd har skrivit ligger kvar i den trådens minnesarea och inte är synligt för den tråd som returnerar fältets värde. Om fältet är av en primitiv typ kan detta lösas genom att fältet görs volatile.

Double-check idiom

Betrakta metoden instance() i klassen LazySingletonCounter i avsnittet om fullständigt synkroniserade objekt:
public static synchronized LazySingletonCounter instance() {
    if (s == null) {
        s = new LazySingletonCounter();
    }
    return s;
}

Metoden är synkroniserad därför att inte flera trådar ska komma in i den samtidigt och alla upptäcka att s är null och därmed skapa varsin instans av LazySingletonCounter. Men om s faktiskt inte är null gör metoden inget annat än att returnera s och då behöver den inte vara synkroniserad (se föregående stycke). Går det att lösa problemet att kollen if (s == null) ska utföras i ett synchronized-block om s är null men utanför blocket om s inte är null? Ja, med nedanstående kod, vilken är en välkänd variant av föregående exempel. Detta sätt att utföra kollen två gånger brukar kallas double-check idiom.
public static LazySingletonCounter instance() {
    if (s == null) {
        synchronized (this) {
            if (s == null) {
                s = new LazySingletonCounter();
            }
        }
    }
    return s;
}

Open calls

public class ClassWithState {
    private int state = 7;

    public synchronized void updateState() {
        state = ...; //Some calculation that must be executed in a synchronized block.
        anotherObject.longMethodThatDoesNotUseState();
    }

}
Metoden updateState() ovan är synkroniserad därför att beräkningen av state inte får bli avbruten. Men om anotherObject.longMethodThatDoesNotUseState() inte behöver synkroniseras kommer andra trådar att få vänta i onödan på att den returnerar. Detta problem löses enkelt genom att det senare anropet utförs osynkroniserat enligt nedan.
public class ClassWithState {
    private int state = 7;

    public void updateState() {
        synchronized (this) {
            state = ...; //Some calculation that must be executed in a synchronized block.
        }
        anotherObject.longMethodThatDoesNotUseState();
    }

}

Dela upp synchronized-block (2.4.2)

Dela upp klasser

Ponera att vi har en klass Shape som hanterar x-koordinat, y-koordinat, bredd och höjd för någon krumelur på skärmen. Alla metoder i klassen är synkroniserade och det enda lås som används är låset till this. Antag nu att positionen på skärmen (x- och y-koordinat) är helt oberoende av dimensionen (bredd och höjd). Det är då slöseri med tid att en operation på dimensionen ska behöva vänta på en operation på positionen och vice versa. Detta kan vi lösa genom att införa två nya klasser, Location och Dimension, vilka representerar position och dimension. Klassen Shape får nu delegera till dessa nya klasser, DelegatingShape.java. All synkronisering sker nu i delegaterna vilket innebär att operationer kan ske samtidigt på position och dimension.

Detta sätt att dela upp klasser kan förbättra prestandan för trådade program men det är i all objektorienterad programmering lämpligt att utföra sådan uppdelning för att göra programmet mer lättförstått. Faktum är att denna typ av uppdelning är en av de främsta anledningarna till att över huvud taget använda objektorientering.

Dela upp lås

Om det av någon anledning inte skulle gå att dela Shape enligt ovan kan vi åtminstone erhålla prestandavinsten genom dela upp låset så att vi har separata lås för position och dimension. Eftersom det i Java finns ett lås per objekt skapar vi helt enkelt ett nytt objekt för varje lås, sedan synkroniserar vi på de nya "lås"-objekten. Här kommer den varianten, LockSplitShape.java. När operationer ska göras på positionen synkroniserar vi på locationLock och när operationer ska göras på dimensionen synkroniserar vi på dimensionLock. Eftersom vi inte ska använda låsobjekten till något annat än just att utnyttja deras lås räcker det att de är Object. Observera att låsen är final, det finns ingen anledning att någonsin byta ut dem mot andra låsobjekt.

Copy-on-Write (2.4.4)

Om en klass innehåller flera fält som inte är oberoende av varandra utan måste uppdateras samtidigt kan det bli komplicerat att designa synkroniseringen så att inte någon tråd operar på värden som inte hör ihop. Antag till exempel att x- och y-koordinaterna i de olika varianterna av Shape ovan inte fick uppdateras oberoende av varandra. Hur ska vi garantera att inte följande scenario inträffar: tråd 1 hämtar x (genom att anropa getX()), blir avbruten, tråd 2 uppdaterar koordinaterna, tråd 1 exekverar igen och hämtar y (som inte hör ihop med det nyss hämtade värdet på x)? Det hjälper inte att synkronisera getX() och getY() eftersom en tråd kan bli avbruten när bara en av dem är anropad. Lösningen är att alla metoder i Shape måste hantera både x och y. Ett enkelt sätt att garantera att så sker är att inte representera x oxh y med tvåint utan med en klass som innehåller båda två och inte tillåter att de ändrar värde. En sådan klass kan se ut så här, ImmutableLocation.java. Gör vi om Shape så att denna används är det ingen risk att x och y kommer i otakt. I ShapeWithImmutablePoint.java måste x och y alltid hanteras tillsammans.