Introduksjon til Java Memory Model

Java Memory Model (JMM) beskriver oppførselen til tråder i Java runtime-miljøet. Minnemodellen er en del av semantikken til Java-språket, og beskriver hva en programmerer kan og ikke bør forvente når han utvikler programvare ikke for en spesifikk Java-maskin, men for Java som helhet.

Den originale Java-minnemodellen (som spesielt refererer til "perkolokalt minne"), utviklet i 1995, regnes som en fiasko: mange optimaliseringer kan ikke gjøres uten å miste garantien for kodesikkerhet. Spesielt er det flere alternativer for å skrive flertråds "single":

  • enten vil hver handling med å få tilgang til en singleton (selv når objektet ble opprettet for lenge siden, og ingenting kan endres) forårsake en inter-thread lock;
  • eller under et bestemt sett av omstendigheter, vil systemet utstede en uferdig enstøing;
  • eller under et visst sett av omstendigheter, vil systemet skape to enstøinger;
  • eller designet vil avhenge av oppførselen til en bestemt maskin.

Derfor har minnemekanismen blitt redesignet. I 2005, med utgivelsen av Java 5, ble en ny tilnærming presentert, som ble ytterligere forbedret med utgivelsen av Java 14.

Den nye modellen er basert på tre regler:

Regel #1 : Enkeltrådede programmer kjører pseudo-sekvensielt. Dette betyr: i virkeligheten kan prosessoren utføre flere operasjoner per klokke, samtidig endre rekkefølgen, men alle dataavhengigheter forblir, så atferden skiller seg ikke fra sekvensiell.

Regel nummer 2 : det er ingen ut av ingensteds verdier. Å lese en hvilken som helst variabel (unntatt ikke-flyktig lang og dobbel, som denne regelen kanskje ikke gjelder for) vil returnere enten standardverdien (null) eller noe skrevet der av en annen kommando.

Og regel nummer 3 : resten av hendelsene utføres i rekkefølge, hvis de er forbundet med et strengt delordreforhold "utfører før" ( skjer før ).

Skjer før

Leslie Lamport kom opp med konseptet Happens før . Dette er en streng delordensrelasjon introdusert mellom atomkommandoer (++ og -- er ikke atomiske) og betyr ikke "fysisk før".

Den sier at det andre laget vil være "in the know" om endringene gjort av det første.

Skjer før

For eksempel utføres den ene før den andre for slike operasjoner:

Synkronisering og monitorer:

  • Fange skjermen ( låsemetode , synkronisert start) og hva som skjer i samme tråd etter den.
  • Retur av skjermen (metode opplåsing , slutten av synkronisert) og hva som skjer på samme tråd før den.
  • Returnerer skjermen og fanger den deretter av en annen tråd.

Skrive og lese:

  • Skrive til en hvilken som helst variabel og deretter lese den i samme strøm.
  • Alt i samme tråd før du skriver til den flyktige variabelen, og selve skrivingen. flyktig lesning og alt på samme tråd etter det.
  • Å skrive til en flyktig variabel og så lese den på nytt. En flyktig skriving samhandler med minnet på samme måte som en monitorretur, mens en lesing er som en fangst. Det viser seg at hvis en tråd skrev til en flyktig variabel, og den andre fant den, blir alt som går foran skrivingen utført før alt som kommer etter lesingen; se bilde.

Objektvedlikehold:

  • Statisk initialisering og alle handlinger med alle forekomster av objekter.
  • Skriving til siste felt i konstruktøren og alt etter konstruktøren. Som et unntak kobles ikke skjer-før-relasjonen transitivt til andre regler og kan derfor forårsake et løp mellom tråder.
  • Alt arbeid med objektet og finalize() .

Strømmetjeneste:

  • Starte en tråd og eventuell kode i tråden.
  • Nullstilling av variabler relatert til tråden og eventuell kode i tråden.
  • Kode i tråd og join() ; kode i tråden og isAlive() == usann .
  • avbryt() tråden og oppdage at den har stoppet.

Skjer før arbeidsnyanser

Frigjøring av en skjer-før-monitor skjer før du anskaffer samme monitor. Det er verdt å merke seg at det er utløsningen, og ikke utgangen, det vil si at du ikke trenger å bekymre deg for sikkerheten når du bruker ventetid.

La oss se hvordan denne kunnskapen vil hjelpe oss å korrigere vårt eksempel. I dette tilfellet er alt veldig enkelt: bare fjern den eksterne sjekken og la synkroniseringen være som den er. Nå vil den andre tråden garantert se alle endringene, fordi den får skjermen først etter at den andre tråden slipper den. Og siden han ikke vil slippe den før alt er initialisert, vil vi se alle endringene på en gang, og ikke separat:

public class Keeper {
    private Data data = null;

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

        return data;
    }
}

Å skrive til en flyktig variabel skjer - før lesing fra den samme variabelen. Endringen vi har gjort, fikser selvfølgelig feilen, men den setter den som skrev den opprinnelige koden tilbake der den kom fra – blokkering hver gang. Det flyktige søkeordet kan spare. Faktisk betyr det aktuelle utsagnet at når vi leser alt som er erklært flyktig, vil vi alltid få den faktiske verdien.

I tillegg, som jeg sa tidligere, for flyktige felt er skriving alltid (inkludert lang og dobbel) en atomoperasjon. Et annet viktig poeng: hvis du har en flyktig enhet som har referanser til andre enheter (for eksempel en array, List eller en annen klasse), vil bare en referanse til selve enheten alltid være "fersk", men ikke til alt i det kommer inn.

Så tilbake til våre dobbeltlåsende ramser. Ved å bruke volatile kan du fikse situasjonen slik:

public class Keeper {
    private volatile Data data = null;

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

Her har vi fortsatt en lås, men bare hvis data == null. Vi filtrerer ut de resterende tilfellene ved å bruke flyktig lesing. Korrekthet sikres ved at flyktig lagring skjer-før flyktig lesing, og alle operasjoner som skjer i konstruktøren er synlige for den som leser verdien av feltet.