Dobbeltkontrollert låsing med Singleton

1. Introduksjon

I denne opplæringen vil vi snakke om det dobbeltsjekkede låsemønsteret. Dette mønsteret reduserer antall låseanskaffelser ved å sjekke låsetilstanden på forhånd. Som et resultat av dette er det vanligvis et ytelsesløft.

La oss se nærmere på hvordan det fungerer.

2. Implementering

Til å begynne med, la oss vurdere en enkel singleton med drakonisk synkronisering:

offentlig klasse DraconianSingleton {privat statisk DraconianSingleton-forekomst; offentlig statisk synkronisert DraconianSingleton getInstance () {if (forekomst == null) {forekomst = ny DraconianSingleton (); } returnere forekomst; } // privat konstruktør og andre metoder ...}

Til tross for at denne klassen er trådsikker, kan vi se at det er en klar ytelsesulempe: hver gang vi ønsker å få forekomsten av vår singleton, må vi skaffe oss en potensielt unødvendig lås.

For å fikse det, vi kan i stedet starte med å verifisere om vi trenger å lage objektet i utgangspunktet, og bare i så fall vil vi anskaffe låsen.

Når vi går videre, vil vi utføre den samme kontrollen igjen så snart vi går inn i den synkroniserte blokken, for å holde operasjonen atomisk:

offentlig klasse DclSingleton {privat statisk ustabil DclSingleton-forekomst; offentlig statisk DclSingleton getInstance () {if (forekomst == null) {synkronisert (DclSingleton. klasse) {hvis (forekomst == null) {forekomst = ny DclSingleton (); }}} returner forekomst; } // privat konstruktør og andre metoder ...}

En ting å huske på med dette mønsteret er at feltet må være flyktige for å forhindre problemer med usammenheng med hurtigbuffer. Faktisk tillater Java-minnemodellen publisering av delvis initialiserte objekter, og dette kan igjen føre til subtile feil.

3. Alternativer

Selv om den dobbeltsjekkede låsingen potensielt kan øke hastigheten, har den minst to problemer:

  • siden det krever flyktige søkeord for å fungere ordentlig, er det ikke kompatibelt med Java 1.4 og lavere versjoner
  • det er ganske ordentlig og det gjør koden vanskelig å lese

Av disse grunnene, la oss se på noen andre alternativer uten disse feilene. Alle følgende metoder delegerer synkroniseringsoppgaven til JVM.

3.1. Tidlig initialisering

Den enkleste måten å oppnå trådsikkerhet er å integrere objektopprettelsen eller bruke en tilsvarende statisk blokk. Dette utnytter det faktum at statiske felt og blokker initialiseres etter hverandre (Java Language Specification 12.4.2):

offentlig klasse EarlyInitSingleton {privat statisk slutt EarlyInitSingleton INSTANCE = ny EarlyInitSingleton (); offentlig statisk EarlyInitSingleton getInstance () {return INSTANCE; } // privat konstruktør og andre metoder ...}

3.2. Initialisering etter behov

I tillegg, siden vi vet fra referansen til Java Language Specification i forrige avsnitt at en klasseinitialisering skjer første gang vi bruker en av metodene eller feltene, kan vi bruke en nestet statisk klasse til å implementere lat initialisering:

offentlig klasse InitOnDemandSingleton {privat statisk klasse InstanceHolder {privat statisk slutt InitOnDemandSingleton INSTANCE = ny InitOnDemandSingleton (); } offentlig statisk InitOnDemandSingleton getInstance () {return InstanceHolder.INSTANCE; } // privat konstruktør og andre metoder ...}

I dette tilfellet InstanceHolder klasse vil tildele feltet første gang vi får tilgang til det ved å påkalle getInstance.

3.3. Enum Singleton

Den siste løsningen kommer fra Effektiv Java bok (Item 3) av Joshua Block og bruker en enum i stedet for en klasse. I skrivende stund anses dette å være den mest konsise og trygge måten å skrive en singleton på:

offentlig enum EnumSingleton {INSTANCE; // andre metoder ...}

4. Konklusjon

For å oppsummere gikk denne raske artikkelen gjennom det dobbeltkontrollerte låsemønsteret, dets grenser og noen alternativer.

I praksis gjør overdreven ordethet og mangel på bakoverkompatibilitet dette mønsteret feilutsatt, og derfor bør vi unngå det. I stedet bør vi bruke et alternativ som lar JVM synkronisere.

Som alltid er koden til alle eksemplene tilgjengelig på GitHub.