Introduktion till Java Memory Model

Java Memory Model (JMM) beskriver beteendet hos trådar i Java runtime-miljön. Minnesmodellen är en del av Java-språkets semantik, och beskriver vad en programmerare kan och inte bör förvänta sig när man utvecklar programvara inte för en specifik Java-maskin, utan för Java som helhet.

Den ursprungliga Java-minnesmodellen (som i synnerhet refererar till "perkolokalt minne"), utvecklad 1995, anses vara ett misslyckande: många optimeringar kan inte göras utan att förlora garantin för kodsäkerhet. I synnerhet finns det flera alternativ för att skriva flertrådad "singel":

  • antingen kommer varje handling av att komma åt en singelton (även när objektet skapades för länge sedan, och ingenting kan ändras) orsaka ett inter-trådlås;
  • eller under en viss uppsättning omständigheter kommer systemet att utfärda en oavslutad ensamvarg;
  • eller under en viss uppsättning omständigheter kommer systemet att skapa två ensamvargar;
  • eller så kommer designen att bero på beteendet hos en viss maskin.

Därför har minnesmekanismen designats om. 2005, med lanseringen av Java 5, presenterades ett nytt tillvägagångssätt, som förbättrades ytterligare med lanseringen av Java 14.

Den nya modellen bygger på tre regler:

Regel #1 : Enkeltrådade program körs pseudo-sekventiellt. Detta betyder: i verkligheten kan processorn utföra flera operationer per klocka, samtidigt som de ändrar sin ordning, men alla databeroenden kvarstår, så beteendet skiljer sig inte från sekventiellt.

Regel nummer 2 : det finns inga värden från ingenstans. Om du läser valfri variabel (förutom icke-flyktiga långa och dubbla, för vilka denna regel kanske inte gäller) returneras antingen standardvärdet (noll) eller något som skrivits där av ett annat kommando.

Och regel nummer 3 : resten av händelserna exekveras i ordning, om de är sammankopplade med ett strikt partiell ordningsförhållande "exekveras före" ( händer före ) .

Händer tidigare

Leslie Lamport kom på konceptet Happens förut . Detta är en strikt partiell ordningsrelation som introduceras mellan atomkommandon (++ och -- är inte atomära) och betyder inte "fysiskt tidigare".

Det står att det andra laget kommer att vara "medvetet" om ändringarna som gjorts av det första.

Händer tidigare

Till exempel exekveras den ena före den andra för sådana operationer:

Synkronisering och monitorer:

  • Fånga monitorn ( låsmetod , synkroniserad start) och vad som än händer i samma tråd efter den.
  • Retur av monitorn (metod upplåsning , slutet av synkroniserad) och vad som än händer i samma tråd innan den.
  • Lämna tillbaka monitorn och sedan fånga den med en annan tråd.

Att skriva och läsa:

  • Att skriva till valfri variabel och sedan läsa den i samma ström.
  • Allt i samma tråd innan man skriver till den flyktiga variabeln, och själva skrivningen. flyktig läsning och allt på samma tråd efter det.
  • Att skriva till en flyktig variabel och sedan läsa den igen. En flyktig skrivning interagerar med minnet på samma sätt som en monitorretur, medan en läsning är som en fångst. Det visar sig att om en tråd skrev till en flyktig variabel, och den andra hittade den, så exekveras allt som föregår skrivningen före allt som kommer efter läsningen; se bild.

Objektunderhåll:

  • Statisk initiering och alla åtgärder med alla instanser av objekt.
  • Skriver till sista fält i konstruktören och allt efter konstruktören. Som ett undantag ansluter händer-före-relationen inte transitivt till andra regler och kan därför orsaka ett lopp mellan trådarna.
  • Allt arbete med objektet och finalize() .

Streamtjänst:

  • Starta en tråd och eventuell kod i tråden.
  • Nollställning av variabler relaterade till tråden och eventuell kod i tråden.
  • Koda i tråden och join() ; kod i tråden och isAlive() == false .
  • interrupt() tråden och upptäck att den har stannat.

Händer före arbetsnyanser

Att släppa en händer-före-monitor sker innan man skaffar samma monitor. Det är värt att notera att det är releasen och inte utgången, det vill säga du behöver inte oroa dig för säkerheten när du använder vänta.

Låt oss se hur denna kunskap kommer att hjälpa oss att korrigera vårt exempel. I det här fallet är allt väldigt enkelt: ta bara bort den externa kontrollen och lämna synkroniseringen som den är. Nu kommer den andra tråden garanterat att se alla ändringar, eftersom den kommer att få monitorn först efter att den andra tråden släppt den. Och eftersom han inte kommer att släppa det förrän allt är initierat, kommer vi att se alla ändringar på en gång, och inte separat:

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

Att skriva till en flyktig variabel sker - innan man läser från samma variabel. Ändringen vi har gjort fixar naturligtvis felet, men det sätter tillbaka den som skrev den ursprungliga koden där den kom ifrån - blockerar varje gång. Det flyktiga nyckelordet kan spara. Faktum är att uttalandet i fråga innebär att när vi läser allt som deklareras flyktigt så kommer vi alltid att få det faktiska värdet.

Dessutom, som jag sa tidigare, för flyktiga fält är skrivning alltid (inklusive lång och dubbel) en atomoperation. En annan viktig punkt: om du har en flyktig enhet som har referenser till andra enheter (till exempel en array, List eller någon annan klass), så kommer endast en referens till själva enheten alltid att vara "färsk", men inte till allt i det kommer in.

Så, tillbaka till våra dubbellåsande kolvar. Med hjälp av volatile kan du fixa situationen så här:

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

Här har vi fortfarande ett lås, men bara om data == null. Vi filtrerar bort de återstående fallen med flyktig läsning. Korrektheten säkerställs av det faktum att volatile store händer-före volatile read, och alla operationer som sker i konstruktorn är synliga för den som läser värdet av fältet.