Osäker trådsäkerhet i Java på multiprocessorsystem
Jag läste en intressant och nästan lite oroande intervju med Cliff Click i TheServerSide om Javas minnesmodell, trådsäkerhet, multiprocessorsystem och problem runt detta. Flera CPUer eller andra CPU-familjer (speciellt icke-x86 CPUer) ger komplikationer för multitrådade javaprogram. Det finns risk att ditt program slutar fungera, går betydligt långsammare eller timeoutar om du lämnar din enprocessormiljö.
Intervjun är tekniskt avancerad och lärorik – läs den!. För er som inte orkar läsa den följer här en sammanfattning av vad jag tyckte var intressant och som jag fattade något av.
volatile och synchronized för idiomet ”double checked locking”
Double checked locking är ett (ö)känt idiom som används för att slippa prestandaförlust pga en singelton (icke static) som skyddas med synchronized i varje getInstance() till densamma. Principen är att synchronized bara behövs när singeltonen skall skapas och sedan kan man använda NULL-kontroll istället.
Tidigare har double checked locking varit ett icke fungerande idiom. Numera har dock JVM’s och Javas minnesmodell updaterats och med volatile kan man nu få detta att fungera.
När du läser en variabel som skall referera en singelton och finner den icke-null så innebär detta inte att singeltonens eget data är initierat klart trots att man i koden tilldelar variabeln efter att konstruktorn är utförd. Här måste alltså volatile användas för att säkerställa detta.
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
}
Jag lärde mig hur volatile egentligen fungerar i Java; att det inte bara som för C innebär att datat måste skrivas/läsa i RAM-minnet (ej cachas) för varje access i koden utan även att ordningen på volatile-accesser bibehålls map andra volatile OCH icke-volatile variabler. Detta ger därför samma effekt som ett vanligt synchronized-lås i aspekten av datasynkronisering och även ungefär samma overhead. För att åstadkomma detta måste VMen/JITen styra CPUn till att inte ändra ordningen och för att synkronisera data mot minnet.
volatile förklaras tydligare i följande FAQ än i intervjun.
Fairness
Med fair i detta sammanhang avses att alla aktiva trådar scheduleras ”rättvist” så att ingen tråd blir utsvält (starvation). För lås innebär fairness att man blir ”insläppt” i den ordning man kom (som en kö).
Det pratas om att unfairness kan leda till att en tråd inte ”kommer vidare” och tex leder till request timeout ifall man har fler körande trådar än CPUer.
”Scheduling of that kind is not part of the Java language spec at all. You know, there is even priorities given in the language spec for thread priorities, but they’re very loosely defined it’s what their behavior is supposed to do. So right, if you have more runnable threads than CPUs, you have no guarantee at all that the extra runnables will ever see the light of day.”
Cliff hävdar att fairness bara kan åstadkommas ifall det stöds av både OSet och hårdvaran.
Allt detta låter lite skrämmande tycker jag. Jag undrar hur stort problem detta är i praktiken?
Låsfri trådsäkerhet
Det diskuteras kring ”lock free safety” och ”lock free collections”. Kan vi skippa lås helt på ett smart sätt?
Cliff har gjort en låsfri implementation av en HashMap på SourceForge som kallas high scale lib som är trådsäker. Cliff har visat på JavaOne att denna är överlägsen ConcurrentHashMap. Han har modellerat operationerna på en HashTable som tillståndsövergångar i en tillståndsmaskin och formellt visat att det inte spelar någon roll i vilken ordning de görs (pga flertrådig access). Coolt!
Han säger dock att just i detta specialfall gick det att implementera utan lås men det finns som vanligt ingen generell lösning på låsfri trådsäkerhet.
Råd
Avslutningsvis nämns lite saker man kan göra för att underlätta det flertrådiga livet:
- Använda JDK1.5 java.util.concurrent
- Använd work/thread pools för jobb med oberoende data i separata trådar.
- Testa på exakt den HW/OS-plattform som är målplattform; ”nästan samma” är riskabelt och svårbedömt. Förvänta dig inte att koden funkar att flytta från x86 till tex Itanium bara för att den är vältestad på x86.
- Använd verktyget FindBugs, finns även för Maven.