Vejledning til det flygtige nøgleord i Java

1. Oversigt

I mangel af nødvendige synkroniseringer kan kompilatoren, runtime eller processorer muligvis anvende alle mulige optimeringer. Selvom disse optimeringer er gavnlige det meste af tiden, kan de nogle gange forårsage subtile problemer.

Cache og omorganisering er blandt de optimeringer, der kan overraske os i samtidige sammenhænge. Java og JVM giver mange måder at kontrollere hukommelsesrækkefølgen på og flygtige nøgleord er en af ​​dem.

I denne artikel vil vi fokusere på dette grundlæggende, men ofte misforståede koncept på Java-sproget - flygtige nøgleord. Først starter vi med lidt baggrund om, hvordan den underliggende computerarkitektur fungerer, og så bliver vi fortrolige med hukommelsesrækkefølgen i Java.

2. Delt multiprocessorarkitektur

Processorer er ansvarlige for at udføre programinstruktioner. Derfor skal de hente både programinstruktioner og nødvendige data fra RAM.

Da CPU'er er i stand til at udføre et betydeligt antal instruktioner pr. Sekund, er hentning fra RAM ikke det ideelle for dem. For at forbedre denne situation bruger processorer tricks som Out of Order Execution, Branch Prediction, Speculative Execution og selvfølgelig Caching.

Det er her, følgende hukommelseshierarki kommer i spil:

Da forskellige kerner udfører flere instruktioner og manipulerer flere data, udfylder de deres cacher med mere relevante data og instruktioner. Dette vil forbedre den samlede præstation på bekostning af indførelse af cache-sammenhængsudfordringer.

Enkelt sagt skal vi tænke to gange over, hvad der sker, når en tråd opdaterer en cachelagret værdi.

3. Hvornår skal du bruge det? flygtige

For at udvide mere om cache-sammenhængen, lad os låne et eksempel fra bogen Java Concurrency in Practice:

offentlig klasse TaskRunner {privat statisk int-nummer; privat statisk boolsk klar; privat statisk klasse Læser udvider tråd {@Override public void run () {while (! ready) {Thread.yield (); } System.out.println (nummer); }} offentlig statisk ugyldig hoved (String [] args) {new Reader (). start (); antal = 42; klar = sand; }}

Det TaskRunner klasse opretholder to enkle variabler. I sin hovedmetode opretter den en anden tråd, der drejer på parat variabel, så længe den er falsk. Når variablen bliver rigtigt, tråden vil blot udskrive nummer variabel.

Mange kan forvente, at dette program blot udskriver 42 efter en kort forsinkelse. I virkeligheden kan forsinkelsen dog være meget længere. Det kan endda hænge for evigt eller endda udskrive nul!

Årsagen til disse uregelmæssigheder er manglen på korrekt hukommelsessynlighed og omorganisering. Lad os evaluere dem mere detaljeret.

3.1. Hukommelsessynlighed

I dette enkle eksempel har vi to applikationstråde: hovedtråden og læsertråden. Lad os forestille os et scenario, hvor operativsystemet planlægger disse tråde på to forskellige CPU-kerner, hvor:

  • Hovedtråden har sin kopi af parat og nummer variabler i dens kernecache
  • Læsertråden ender også med sine kopier
  • Hovedtråden opdaterer de cachelagrede værdier

På de fleste moderne processorer anvendes skriveanmodninger ikke med det samme, efter at de er udstedt. Faktisk, processorer har tendens til at sætte køerne i kø i en speciel skrivebuffer. Efter et stykke tid vil de anvende disse skriv på hovedhukommelsen på én gang.

Med alt det sagt når hovedtråden opdaterer nummer og parat variabler, er der ingen garanti for, hvad læsertråden kan se. Med andre ord kan læsertråden muligvis se den opdaterede værdi med det samme eller med en vis forsinkelse eller aldrig overhovedet!

Denne hukommelsessynlighed kan forårsage livsproblemer i programmer, der er afhængige af synlighed.

3.2. Ombestilling

For at gøre tingene endnu værre, læsertråden kan se disse skriver i en hvilken som helst anden rækkefølge end den aktuelle programrækkefølge. For eksempel, siden vi først opdaterer nummer variabel:

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

Vi kan forvente, at læsertråden udskrives 42. Det er dog faktisk muligt at se nul som den trykte værdi!

Ombestillingen er en optimeringsteknik til præstationsforbedringer. Interessant nok kan forskellige komponenter anvende denne optimering:

  • Processoren kan skylle sin skrivebuffer i en hvilken som helst anden rækkefølge end programordren
  • Processoren kan anvende teknik, der ikke er i orden
  • JIT-kompilatoren optimeres muligvis via ombestilling

3.3. flygtige Hukommelsesrækkefølge

For at sikre, at opdateringer til variabler spredes forudsigeligt til andre tråde, skal vi anvende flygtige modifikator til disse variabler:

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

På denne måde kommunikerer vi med runtime og processor for ikke at ombestille nogen instruktion, der involverer flygtige variabel. Også processorer forstår, at de med det samme skal skylle opdateringer til disse variabler.

4. flygtige og trådsynkronisering

For applikationer med flere tråde er vi nødt til at sikre et par regler for ensartet adfærd:

  • Gensidig udelukkelse - kun en tråd udfører et kritisk afsnit ad gangen
  • Synlighed - ændringer foretaget af en tråd til de delte data er synlige for andre tråde for at opretholde datakonsistens

synkroniseret metoder og blokke tilvejebringer begge ovenstående egenskaber på bekostning af applikationsydelse.

flygtige er et ganske nyttigt nøgleord, fordi det kan hjælpe med at sikre synlighedsaspektet ved dataændringen uden naturligvis at give gensidig udelukkelse. Således er det nyttigt de steder, hvor det er ok med flere tråde, der udfører en blok kode parallelt, men vi skal sikre synlighedsegenskaben.

5. sker før bestilling

Hukommelsens synlighedseffekter af flygtige variabler strækker sig ud over flygtige variabler selv.

Lad os antage, at tråd A skriver til a for at gøre sagen mere konkret flygtige variabel, og derefter læser tråd B det samme flygtige variabel. I sådanne tilfælde, de værdier, der var synlige for A, før de skrev flygtige variabel vil være synlig for B efter at have læst flygtige variabel:

Teknisk set skriver enhver til en flygtige felt sker inden hver efterfølgende læsning af det samme felt. Dette er flygtige variabel regel for Java Memory Model (JMM).

5.1. Piggybacking

På grund af styrken af ​​det der sker før hukommelsesbestilling, kan vi nogle gange piggyback på synlighedsegenskaberne hos en anden flygtige variabel. For eksempel i vores særlige eksempel er vi bare nødt til at markere parat variabel som flygtige:

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

Alt inden skrivning rigtigt til parat variabel er synlig for noget efter at have læst parat variabel. Derfor er den nummer variable piggybacks på hukommelsessynligheden håndhævet af parat variabel. Enkelt sagt, selvom det ikke er en flygtige variabel, viser den en flygtige opførsel.

Ved at bruge denne semantik kan vi kun definere nogle få af variablerne i vores klasse som flygtige og optimer synlighedsgarantien.

6. Konklusion

I denne vejledning har vi udforsket mere om flygtige nøgleord og dets kapaciteter samt forbedringerne af det startende med Java 5.

Som altid kan kodeeksemplerne findes på GitHub.