Veiledning til det flyktige nøkkelordet i Java

1. Oversikt

I mangel av nødvendige synkroniseringer kan kompilatoren, kjøretiden eller prosessorene bruke alle slags optimaliseringer. Selv om disse optimaliseringene er gunstige det meste av tiden, kan de noen ganger forårsake subtile problemer.

Cache og omorganisering er blant de optimaliseringene som kan overraske oss i samtidige sammenhenger. Java og JVM gir mange måter å kontrollere minneordren og flyktige nøkkelord er en av dem.

I denne artikkelen vil vi fokusere på dette grunnleggende, men ofte misforståtte konseptet på Java-språket - the flyktige nøkkelord. Først begynner vi med litt bakgrunn om hvordan den underliggende dataarkitekturen fungerer, og deretter blir vi kjent med minneordren i Java.

2. Delt multiprosessorarkitektur

Prosessorer er ansvarlige for å utføre programinstruksjoner. Derfor må de hente både programinstruksjoner og nødvendige data fra RAM.

Siden CPUer er i stand til å utføre et betydelig antall instruksjoner per sekund, er henting fra RAM ikke det ideelle for dem. For å forbedre denne situasjonen bruker prosessorer triks som utføring av ordre, forutsigelse av filialer, spekulativ utførelse og selvfølgelig caching.

Det er her følgende minnehierarki spiller inn:

Ettersom forskjellige kjerner utfører flere instruksjoner og manipulerer mer data, fyller de opp cachene med mer relevante data og instruksjoner. Dette vil forbedre den totale ytelsen på bekostning av å innføre kohærensutfordringer for hurtigbuffer.

Enkelt sagt bør vi tenke to ganger om hva som skjer når en tråd oppdaterer en hurtigbufret verdi.

3. Når du skal bruke flyktige

For å utvide mer om cache-sammenheng, la oss låne ett eksempel fra boken Java Concurrency in Practice:

offentlig klasse TaskRunner {privat statisk int-nummer; privat statisk boolsk klar; privat statisk klasse Leser utvider tråd {@Override public void run () {while (! ready) {Thread.yield (); } System.out.println (nummer); }} public static void main (String [] args) {new Reader (). start (); tall = 42; klar = sann; }}

De TaskRunner klasse opprettholder to enkle variabler. I sin hovedmetode skaper den en annen tråd som snurrer på klar variabel så lenge den er falsk. Når variabelen blir ekte, tråden vil bare skrive ut Nummer variabel.

Mange kan forvente at dette programmet bare skriver ut 42 etter en kort forsinkelse. Imidlertid kan forsinkelsen være mye lenger. Det kan til og med henge for alltid, eller til og med skrive ut null!

Årsaken til disse avvikene er mangelen på riktig minnesynlighet og omorganisering. La oss evaluere dem mer detaljert.

3.1. Minne synlighet

I dette enkle eksemplet har vi to applikasjonstråder: hovedtråden og lesertråden. La oss forestille oss et scenario der operativsystemet planlegger disse trådene på to forskjellige CPU-kjerner, hvor:

  • Hovedtråden har sin kopi av klar og Nummer variabler i kjernebufferen
  • Lesertråden ender også med kopiene
  • Hovedtråden oppdaterer hurtigbufrede verdier

På de fleste moderne prosessorer vil skriveforespørsler ikke bli brukt med en gang etter at de er utstedt. Faktisk, prosessorer pleier å stå i kø for de som skriver i en spesiell skrivebuffer. Etter en stund vil de bruke disse skrivene på hovedminnet samtidig.

Med alt som blir sagt når hovedtråden oppdaterer Nummer og klar variabler, er det ingen garanti for hva lesertråden kan se. Med andre ord kan lesertråden se den oppdaterte verdien med en gang, eller med en viss forsinkelse, eller aldri i det hele tatt!

Denne minnesynligheten kan forårsake livsproblemer i programmer som er avhengige av synlighet.

3.2. Omorganisering

For å gjøre saken enda verre, lesertråden kan se skrivene i hvilken som helst annen rekkefølge enn den faktiske programrekkefølgen. For eksempel siden vi først oppdaterte Nummer variabel:

public static void main (String [] args) {new Reader (). start (); tall = 42; klar = sann; }

Vi kan forvente at lesertrådutskriftene 42. Imidlertid er det faktisk mulig å se null som den trykte verdien!

Omorganiseringen er en optimaliseringsteknikk for ytelsesforbedringer. Interessant, forskjellige komponenter kan bruke denne optimaliseringen:

  • Prosessoren kan skylle skrivebufferen i en hvilken som helst annen rekkefølge enn programrekkefølgen
  • Prosessoren kan bruke utførelsesteknikk uten ordre
  • JIT-kompilatoren kan optimalisere via omorganisering

3.3. flyktige Minneordre

For å sikre at oppdateringer til variabler forplantes forutsigbart til andre tråder, bør vi bruke flyktige modifikator for disse variablene:

offentlig klasse TaskRunner {privat flyktig statisk int-nummer; privat flyktig statisk boolsk klar; // samme som før }

På denne måten kommuniserer vi med kjøretid og prosessor for ikke å omorganisere noen instruksjon som involverer flyktige variabel. Også prosessorer forstår at de skal spyle oppdateringer av disse variablene med en gang.

4. flyktige og trådsynkronisering

For flertrådede applikasjoner må vi sørge for et par regler for konsistent oppførsel:

  • Gjensidig ekskludering - bare en tråd utfører en kritisk del om gangen
  • Synlighet - endringer gjort av en tråd til de delte dataene er synlige for andre tråder for å opprettholde datakonsistens

synkronisert metoder og blokker gir begge de ovennevnte egenskapene, på bekostning av applikasjonsytelsen.

flyktige er et ganske nyttig nøkkelord fordi det kan bidra til å sikre synlighetsaspektet ved dataendringen uten selvfølgelig å gi gjensidig utelukkelse. Dermed er det nyttig på de stedene hvor det er ok med flere tråder som utfører en kodeblokk parallelt, men vi må sikre synlighetsegenskapen.

5. Hender-Før du bestiller

Minne synlighet effekter av flyktige variabler strekker seg utover flyktige variabler i seg selv.

La oss anta at tråd A skriver til a for å gjøre saken mer konkret flyktige variabel, og deretter leser tråd B det samme flyktige variabel. I slike tilfeller, verdiene som var synlige for A før du skrev flyktige variabel vil være synlig for B etter å ha lest flyktige variabel:

Teknisk sett skriver noen til en flyktige feltet skjer før hver påfølgende lesing av det samme feltet. Dette er flyktige variabel regel for Java Memory Model (JMM).

5.1. Piggybacking

På grunn av styrken til det som skjer før minnebestilling, kan vi noen ganger snakke på synlighetsegenskapene til en annen flyktige variabel. For eksempel, i vårt spesielle eksempel, trenger vi bare å merke klar variabel som flyktige:

offentlig klasse TaskRunner {privat statisk int-nummer; // ikke flyktig privat flyktig statisk boolsk klar; // samme som før }

Alt før skriving ekte til klar variabel er synlig for hva som helst etter å ha lest klar variabel. derfor Nummer variable piggybacks på minnesynligheten håndhevet av klar variabel. Enkelt sagt, selv om det ikke er en flyktige variabel, viser den en flyktige oppførsel.

Ved å bruke denne semantikken kan vi bare definere noen få av variablene i klassen vår som flyktige og optimalisere synlighetsgarantien.

6. Konklusjon

I denne opplæringen har vi utforsket mer om flyktige nøkkelord og dets evner, samt forbedringene som ble gjort til det, med Java 5.

Som alltid kan kodeeksemplene finnes på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found