Dobbelttjekket låsning uden flygtige
Første ting først:Det, du forsøger at gøre, er i bedste fald farligt. Jeg bliver lidt nervøs, når folk prøver at snyde med finaler. Java-sproget giver dig volatile
som det bedste værktøj til at håndtere sammenhæng mellem tråde. Brug det.
Under alle omstændigheder er den relevante tilgang beskrevet i "Sikker udgivelse og initialisering i Java" som:
public class FinalWrapperFactory {
private FinalWrapper wrapper;
public Singleton get() {
FinalWrapper w = wrapper;
if (w == null) { // check 1
synchronized(this) {
w = wrapper;
if (w == null) { // check2
w = new FinalWrapper(new Singleton());
wrapper = w;
}
}
}
return w.instance;
}
private static class FinalWrapper {
public final Singleton instance;
public FinalWrapper(Singleton instance) {
this.instance = instance;
}
}
}
Det er lægmandsbetingelser, det fungerer sådan her. synchronized
giver den korrekte synkronisering, når vi observerer wrapper
som null -- med andre ord, koden ville naturligvis være korrekt, hvis vi dropper den første kontrol helt og udvider synchronized
til hele metodekroppen. final
i FinalWrapper
garanterer, hvis vi så ikke-null wrapper
, den er fuldt konstrueret, og alle Singleton
felter er synlige -- dette gendannes fra den hurtige læsning af wrapper
.
Bemærk, at den overfører FinalWrapper
i marken, ikke selve værdien. Hvis instance
skulle udgives uden FinalWrapper
, ville alle væddemål være slået fra (i lægmandsforstand er det for tidlig offentliggørelse). Det er derfor din Publisher.publish
er disfunktionel:bare at sætte værdien gennem det sidste felt, læse den tilbage og udgive den på en usikker måde er ikke sikkert -- det minder meget om blot at sætte den nøgne instance
skriv ud.
Du skal også være forsigtig med at lave en "tilbagegangs"-læsning under låsen, når du opdager null wrapper
, og brug dens værdi . Udfører anden (tredje) læsning af wrapper
til gengæld ville erklæringen også ødelægge rigtigheden, hvilket sætter dig op til et legitimt løb.
EDIT:Hele den ting siger i øvrigt, at hvis objektet du udgiver er dækket med final
-s internt, kan du skære mellemleddet af FinalWrapper
, og udgiv instance
sig selv.
EDIT 2:Se også, LCK10-J. Brug en korrekt form af det dobbelttjekkede låsesprog, og lidt diskussion i kommentarerne der.
Kort sagt
Versionen af koden uden volatile
eller wrapper-klassen er afhængig af hukommelsesmodellen for det underliggende operativsystem, som JVM'en kører på.
Versionen med wrapper-klassen er et kendt alternativ kendt som Initialization on Demand Holder-designmønsteret og er afhængig af ClassLoader
kontrakt om, at en given klasse højst indlæses én gang, ved første adgang og på en trådsikker måde.
Behovet for volatile
Den måde, udviklere tænker på kodeeksekvering det meste af tiden på, er, at programmet indlæses i hovedhukommelsen og køres direkte derfra. Virkeligheden er dog, at der er en række hardware-caches mellem hovedhukommelsen og processorkernerne. Problemet opstår, fordi hver tråd muligvis kører på separate processorer, hver med deres egen uafhængige kopi af variablerne i omfanget; mens vi gerne logisk tænker på field
som et enkelt sted er virkeligheden mere kompliceret.
For at gennemgå et simpelt (omend måske udførligt) eksempel, overvej et scenario med to tråde og et enkelt niveau af hardware-cache, hvor hver tråd har deres egen kopi af field
i den cache. Så der er allerede tre versioner af field
:en i hovedhukommelsen, en i den første kopi og en i den anden kopi. Jeg vil referere til disse som field
M , field
A og field
B hhv.
- Oprindelig tilstand
field
M =null
field
A =null
field
B =null
- Tråd A udfører den første nul-check, finder
field
A er nul. - Tråd A får låsen på
this
. - Tråd B udfører den første nul-check, finder
field
B er nul. - Tråd B forsøger at få låsen på
this
men finder ud af, at den holdes af tråd A. Tråd B sover. - Tråd A udfører den anden nul-check, finder
field
A er nul. - Tråd A tildeler
field
A værdienfieldType1
og udløser låsen. Sidenfield
er ikkevolatile
denne opgave udbredes ikke.
field
M =null
field
A =fieldType1
field
B =null
- Tråd B vågner og får låsen på
this
. - Tråd B udfører den anden nul-check, finder
field
B er nul. - Tråd B tildeler
field
B værdienfieldType2
og udløser låsen.
field
M =null
field
A =fieldType1
field
B =fieldType2
- På et tidspunkt synkroniseres skrivningerne til cachekopi A tilbage til hovedhukommelsen.
field
M =fieldType1
field
A =fieldType1
field
B =fieldType2
- På et senere tidspunkt synkroniseres skrivningerne til cache-kopi B tilbage til hovedhukommelsen overskriver tildelingen lavet af kopi A.
field
M =fieldType2
field
A =fieldType1
field
B =fieldType2
Som en af kommentatorerne på det nævnte spørgsmål ved at bruge volatile
sikrer, at skrivninger er synlige. Jeg kender ikke den mekanisme, der bruges til at sikre dette -- det kan være, at ændringer udbredes til hver kopi, det kan være, at kopierne aldrig bliver lavet i første omgang og alle adgange til field
er imod hovedhukommelsen.
En sidste bemærkning om dette:Jeg nævnte tidligere, at resultaterne er systemafhængige. Dette skyldes, at forskellige underliggende systemer kan tage mindre optimistiske tilgange til deres hukommelsesmodel og behandle alle hukommelse delt på tværs af tråde som volatile
eller måske anvende en heuristik til at bestemme, om en bestemt reference skal behandles som volatile
eller ej, dog på bekostning af ydeevnen ved synkronisering til hovedhukommelsen. Dette kan gøre test for disse problemer til et mareridt; ikke kun skal du køre mod en tilstrækkelig stor prøve for at forsøge at udløse løbstilstanden, du kan bare tilfældigvis teste på et system, som er konservativt nok til aldrig at udløse tilstanden.
Initialisering efter behov
Det vigtigste, jeg ville påpege her, er, at dette virker, fordi vi i det væsentlige sniger en singleton ind i blandingen. ClassLoader
kontrakt betyder, at mens der kan mange forekomster af Class
, kan der kun være en enkelt forekomst af Class<A>
tilgængelig for enhver type A
, som også tilfældigvis indlæses først, når den første reference / dovent initialiseret. Faktisk kan du tænke på ethvert statisk felt i en klasses definition som værende felter i en singleton forbundet med den klasse, hvor der tilfældigvis er øgede medlemsadgangsprivilegier mellem den singleton og forekomster af klassen.
Citerer "Dobbelttjekket låsning er brudt"-erklæringen nævnt af @Kicsi, det allersidste afsnit er:
Dobbeltkontrolleret låsning af uforanderlige objekter
Hvis Helper er et uforanderligt objekt, således at alle felterne iHelper er endelige, så vil dobbelttjekket låsning fungere uden at skulle bruge flygtige felter . Ideen er, at en reference til et uforanderligt objekt (såsom en streng eller et heltal) skal opføre sig på samme måde som en int eller float; læse- og skrivereferencer til uforanderlige genstande er atomare.
(understregningen er min)
Siden FieldHolder
er uforanderlig, behøver du faktisk ikke volatile
søgeord:andre tråde vil altid se en korrekt initialiseret FieldHolder
. Så vidt jeg forstår det, er FieldType
vil således altid blive initialiseret, før den kan tilgås fra andre tråde gennem FieldHolder
.
Korrekt synkronisering er dog stadig nødvendig, hvis FieldType
er ikke uforanderlig. Som følge heraf er jeg ikke sikker på, at du ville have meget gavn af at undgå volatile
søgeord.
Hvis den dog er uforanderlig, behøver du ikke FieldHolder
overhovedet efter ovenstående citat.