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

Guide til det flygtige søgeord i Java

1. Oversigt

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

Caching og genbestilling 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 den flygtige  søgeord er et af dem.

I denne artikel vil vi fokusere på dette grundlæggende, men ofte misforståede koncept i Java-sproget – det flygtige søgeord. Først starter vi med lidt baggrund om, hvordan den underliggende computerarkitektur fungerer, og derefter vil vi blive fortrolige med hukommelsesrækkefølge 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 i sekundet, er hentning fra RAM ikke så ideelt 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:

Efterhånden som forskellige kerner udfører flere instruktioner og manipulerer flere data, fylder de deres cache op med mere relevante data og instruktioner. Dette vil forbedre den overordnede ydeevne på bekostning af introduktion af cachekohærensudfordringer .

Forenklet sagt bør 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 flygtig

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

public class TaskRunner {

    private static int number;
    private static boolean ready;

    private static class Reader extends Thread {

        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }

            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new Reader().start();
        number = 42;
        ready = true;
    }
}

 TaskRunner  klasse opretholder to simple variable. I sin hovedmetode opretter den endnu en tråd, der spinder på den klare  variabel, så længe den er falsk. Når variablen bliver sand,  tråden vil blot udskrive nummeret  variabel.

Mange kan forvente, at dette program blot udskriver 42 efter en kort forsinkelse. Men i virkeligheden kan forsinkelsen 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 scenarie, hvor operativsystemet planlægger disse tråde på to forskellige CPU-kerner, hvor:

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

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

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

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

3.2. Omarrangerer

For at gøre tingene endnu værre kan læsertråden se disse skrifter i enhver anden rækkefølge end den faktiske programrækkefølge . For eksempel siden vi først opdaterer nummeret  variabel:

public static void main(String[] args) { 
    new Reader().start();
    number = 42; 
    ready = true; 
}

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

Genbestillingen er en optimeringsteknik til ydeevneforbedringer. Interessant nok kan forskellige komponenter anvende denne optimering:

  • Processoren kan tømme sin skrivebuffer i enhver anden rækkefølge end programrækkefølgen
  • Behandleren kan anvende udelukket udførelsesteknik
  • JIT-kompileren kan optimere via genbestilling

3.3. flygtig Hukommelsesrækkefølge

For at sikre, at opdateringer til variabler udbredes forudsigeligt til andre tråde, bør vi anvende den flygtige  modifikator til disse variable:

public class TaskRunner {

    private volatile static int number;
    private volatile static boolean ready;

    // same as before
}

På denne måde kommunikerer vi med runtime og processor for ikke at genbestille nogen instruktion, der involverer den flygtige variabel. Processorer forstår også, at de skal fjerne alle opdateringer til disse variabler med det samme.

4. flygtig og trådsynkronisering

For flertrådede applikationer skal vi sikre et par regler for ensartet adfærd:

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

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

flygtig er et ganske nyttigt søgeord, fordi det kan hjælpe med at sikre synlighedsaspektet af dataændringen uden selvfølgelig at give gensidig udelukkelse . Det er således nyttigt de steder, hvor vi er ok med flere tråde, der udfører en kodeblok parallelt, men vi skal sikre synlighedsegenskaben.

5. Sker - før bestilling

Hukommelsessynlighedseffekterne af flygtige variabler strækker sig ud over det flygtige variabler selv.

For at gøre tingene mere konkrete, lad os antage, at tråd A skriver til en flygtig variabel, og så læser tråd B den samme flygtige variabel. I sådanne tilfælde de værdier, der var synlige for A, før du skrev den flygtige variabel vil være synlig for B efter at have læst flygtige variabel:

Teknisk set kan enhver skrive til en flygtig felt sker før hver efterfølgende læsning af det samme felt . Dette er den flygtige variabel regel for Java Memory Model (JMM).

5.1. Piggybacking

På grund af styrken af ​​sker-før-hukommelsesbestillingen kan vi nogle gange piggyback på synlighedsegenskaberne for en anden flygtig variabel . For eksempel skal vi i vores specifikke eksempel blot markere klar  variabel som flygtig :

public class TaskRunner {

    private static int number; // not volatile
    private volatile static boolean ready;

    // same as before
}

Alt før du skriver sandt  til den klare variabel er synlig for alt efter at have læst ready  variabel. Derfor nummeret  variable piggybacks på hukommelsessynlighed håndhævet af ready variabel. Forenklet sagt selvom det ikke er en flygtig variabel, udviser den en flygtig adfærd.

Ved at gøre brug af denne semantik kan vi kun definere nogle få af variablerne i vores klasse som flygtige og optimere synlighedsgarantien.

6. Konklusion

I dette selvstudie har vi udforsket mere om det flygtige nøgleordet og dets muligheder samt de forbedringer, der er lavet til det, der starter med Java 5.

Som altid kan kodeeksemplerne findes på GitHub.


Java tag