De goeie plek
Ok. Het is een vm toegestaan om instructies (bytecode) te gaan reordenen omdat processors hier vaak performance winst mee kunnen halen. Stel dit is de orginele bytecode:
code:
1
2
3
4
5
6
7
8
| //maakt een niet geinitialiseerde versie van
//MyInt aan. value staat nu op 0 omdat de default
//value van een int 0 is.
mem = maak-instantie(MyInt)
//roep nu de constructor aan met 21
constructor-call(mem, 21)
//wijs het geheugen toe aan een globaal veld
globaalveld = mem |
Stel dat 1 thread dit aanroept, en een andere thread leest globaal veld uit, dan zou het null kunnen zien (dus de 2e thread die leest komt voor de uitvoering van de laatste instructie door thread1 aan beurt) of hij leest een MyInt met value 21 uit (nadat de laatste instructie door thread1 is uitgevoerd)
Het is toegestaan voor een vm dus om instructies te gaan rewriten en een mogelijke reordening zou zijn:
code:
1
2
3
| mem = maak-instantie(MyInt)
globaalveld = mem
constructor-call(mem,21) |
Stel dat een 2e thread net voor de uitvoering van de laatste instructie door de 1e thread dat globaal veld uitleest, dan zou het een instantie van MyInt kunnen zien waarvan de constructor nog niet is aangeroepen, het zou dus een MyInt(0) kunnen zien ipv een MyInt(21)
2e oorzaak:
Je zult misschien denken: ok, dat is maar een fractie van de tijd waarop het fout zou kunnen gaan. Alhoewel dit imho geen goed concurrency control is, is het probleem nog groter dan je zou denken.
Dit heeft te maken met het memory model van java. Als je een normaal veld hebt (dus niet volatile, final of gebruikt in een gesynchroniseerde context) dan is de vm niet verplicht om de waarde die erin geschreven gaat worden ook direct naar main memory te sturen. Processors hebben tegenwoordig meerde lagen van cache aan boord en het is veel goedkoper om daar uit te lezen en naar toe te schrijven).
Stel dat we meerdere cpu's hebben (bv multicores) en dat iedere cpu zijn eigen cache heeft.
Stel nu dat thread 1 net het aangemaakt object (waarvan de constructor nog aangeroepen moet worden) naar main memory heeft geschreven. Stel nu dat thread2 direct daarna the value hiervan uitleest (die waarde is 0). Deze waarde zou hij in zijn cache mogen plaatsen (dit zou een cache op een andere cpu kunnen zijn). Stel dat daarna de constructor door thread 1 wordt aangeroepen zodat die waarde:21 in main memory komt te staan, dan is er geen reden voor thread 2 om nu ineens een nieuwe waarde voor value uit te gaan lezen want hij heeft zijn 0 waarde nog mooi in de cache staan. Hij ziet die waarde dus niet en de kans bestaat ook dat hij hem nooit zal zien.
Een andere variant op het laatste probleem:
ook de writes hoeven niet direct geflusht te worden naar main memory. Dus die write van 21 binnen de constructor hoeft ook niet naar main memory geflust te worden. Dus ook al zou thread2 altijd netjes uit main memory lezen ipv cache, dan zou hij die nieuwe waarde nooit hoeven te zien.
Zoals je kunt zien is het allemaal vrij tricky en moet je goed weten wat je moet doen als je bezig bent met concurrency control. Het probleem zou trouwens eenvoudig gefixt kunnen worden door het final te maken. Door het veld final te maken zijn niet allerlei reordening meer mogelijk maar hier ben ik nog niet volledig in thuis (ben op dit moment bezig om de materie te bestuderen).
stel dat
Stel dat thread 2 het niet uitgeconstrueerde object heeft uitgelezen en ook het value veld.
[
Voor 5% gewijzigd door
Alarmnummer op 21-08-2006 22:34
]