Java >> Java tutorial >  >> Tag >> volatile

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.

  1. Oprindelig tilstand
    field M =null
    field A =null
    field B =null
  2. Tråd A udfører den første nul-check, finder field A er nul.
  3. Tråd A får låsen på this .
  4. Tråd B udfører den første nul-check, finder field B er nul.
  5. 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.
  6. Tråd A udfører den anden nul-check, finder field A er nul.
  7. Tråd A tildeler field A værdien fieldType1 og udløser låsen. Siden field er ikke volatile denne opgave udbredes ikke.
    field M =null
    field A =fieldType1
    field B =null
  8. Tråd B vågner og får låsen på this .
  9. Tråd B udfører den anden nul-check, finder field B er nul.
  10. Tråd B tildeler field B værdien fieldType2 og udløser låsen.
    field M =null
    field A =fieldType1
    field B =fieldType2
  11. På et tidspunkt synkroniseres skrivningerne til cachekopi A tilbage til hovedhukommelsen.
    field M =fieldType1
    field A =fieldType1
    field B =fieldType2
  12. 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.


Java tag