Bevezetés a Java memóriamodellbe

A Java memóriamodell (JMM) a szálak viselkedését írja le a Java futási környezetben. A memóriamodell a Java nyelv szemantikájának része, és leírja, hogy a programozó mire számíthat és mit nem szabad, ha nem egy adott Java gépre, hanem a Java egészére fejleszt szoftvert.

Az 1995-ben kifejlesztett eredeti Java memóriamodellt (amely különösen a "perkolokális memóriára" utal) kudarcnak tekintik: sok optimalizálás nem végezhető el anélkül, hogy elveszítené a kódbiztonság garanciáját. Különösen több lehetőség van a többszálú "egyszálú" írására:

  • vagy minden egyes szingli elérési művelet (még akkor is, ha az objektumot régen hozták létre, és semmi sem változhat) a szálak közötti zárolást okoz;
  • vagy bizonyos körülmények között a rendszer befejezetlen magányost bocsát ki;
  • vagy bizonyos körülmények között a rendszer két magányost hoz létre;
  • vagy a kialakítás egy adott gép viselkedésétől függ.

Ezért a memória mechanizmusát újratervezték. 2005-ben, a Java 5 kiadásával egy új megközelítést mutattak be, amelyet a Java 14 kiadásával tovább fejlesztettek.

Az új modell három szabályon alapul:

1. szabály : Az egyszálú programok pszeudoszekvenciálisan futnak. Ez azt jelenti: a valóságban a processzor több műveletet is tud végezni óránként, egyidejűleg megváltoztatva azok sorrendjét, azonban minden adatfüggőség megmarad, így a viselkedés nem tér el a szekvenciálistól.

2. szabály : nincsenek a semmiből értékek. Bármely változó beolvasása (kivéve a nem felejtő long és double, amelyekre ez a szabály nem biztos, hogy érvényes) vagy az alapértelmezett értéket (nulla) adja vissza, vagy egy másik paranccsal odaírt valamit.

És a 3-as szabály : a többi esemény sorrendben kerül végrehajtásra, ha azokat szigorú részleges sorrend köti össze „végrehajtás előtt” ( előtt történik ).

Előtte előfordul

Leslie Lamport korábban kitalálta a Happens koncepcióját . Ez egy szigorú részleges sorrendi összefüggés, amelyet az atomi parancsok között vezettek be (++ és -- nem atomi), és nem azt jelenti, hogy "fizikailag korábban".

Azt írja, hogy a második csapat "tudatában lesz" az első által végrehajtott változtatásokról.

Előtte előfordul

Például az egyik a másik előtt hajtódik végre az ilyen műveletekhez:

Szinkronizálás és monitorok:

  • A monitor rögzítése ( zárolási mód , szinkronizált indítás) és bármi, ami utána ugyanazon a szálon történik.
  • A monitor visszatérése (method unlock , end of synchronized) és bármi, ami előtte ugyanazon a szálon történik.
  • A monitor visszaadása, majd rögzítése egy másik szálon keresztül.

Írás és olvasás:

  • Bármely változóba írás, majd olvasás ugyanabban a folyamban.
  • Minden ugyanabban a szálban, mielőtt az illékony változóba írna, és maga az írás. illékony olvasmány és utána ugyanazon a szálon minden.
  • Írás egy illékony változóba, majd újra elolvassa. Az illékony írás ugyanúgy kölcsönhatásba lép a memóriával, mint a monitor visszatérése, míg az olvasás olyan, mint egy rögzítés. Kiderült, hogy ha az egyik szál írt egy illékony változóba, és a második megtalálta, akkor minden, ami az írást megelőzi, előbb végrehajtódik, mint minden, ami az olvasás után következik; Lásd a képen.

Objektum karbantartás:

  • Statikus inicializálás és bármilyen művelet az objektumok bármely példányával.
  • Írás a végső mezőkbe a konstruktorban és minden a konstruktor után. Kivételként az történik-előtte reláció nem kapcsolódik tranzitív módon más szabályokhoz, ezért szálak közötti versenyt okozhat.
  • Bármilyen munka az objektummal és a finalize() .

Stream szolgáltatás:

  • Szál indítása és a szálban lévő bármely kód.
  • A szálhoz és a szál bármely kódjához kapcsolódó változók nullázása.
  • Kód a szálban és join() ; kód a szálban és isAlive() == false .
  • interrupt() a szálat, és észleli, hogy leállt.

A munka árnyalatai előtt történik

A „történik-előtte” képernyő kiadása ugyanazon monitor beszerzése előtt történik. Érdemes megjegyezni, hogy ez a kioldás, és nem a kilépés, vagyis nem kell aggódnia a biztonság miatt a várakozás használatakor.

Lássuk, hogyan segít ez a tudás a példánk helyesbítésében. Ebben az esetben minden nagyon egyszerű: csak távolítsa el a külső ellenőrzést, és hagyja a szinkronizálást úgy, ahogy van. Most a második szál garantáltan látni fogja az összes változást, mert csak azután kapja meg a monitort, miután a másik szál kiadja. És mivel nem fogja kiadni, amíg minden inicializálásra nem kerül, az összes változást egyszerre fogjuk látni, és nem külön-külön:

public class Keeper {
    private Data data = null;

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

        return data;
    }
}

Az illékony változóba való írás megtörténik, mielőtt ugyanabból a változóból olvasna. Az általunk végrehajtott változtatás természetesen kijavítja a hibát, de aki az eredeti kódot írta, azt visszahelyezi oda, ahonnan származik – minden alkalommal blokkolva. Az illékony kulcsszó menthet. Valójában a szóban forgó állítás azt jelenti, hogy ha mindent olvasunk, ami volatilisnak van nyilvánítva, mindig a tényleges értéket kapjuk.

Ezen túlmenően, ahogy korábban mondtam, az illékony mezők esetében az írás mindig (beleértve a hosszú és dupla) atomi műveletet. Egy másik fontos pont: ha van egy ingadozó entitása, amely más entitásokra hivatkozik (például egy tömbre, Listára vagy más osztályra), akkor csak magára az entitásra való hivatkozás lesz mindig „friss”, de nem mindenre bejövő.

Szóval, vissza a duplazárású kosainkhoz. A volatile használatával a következőképpen javíthatja a helyzetet:

public class Keeper {
    private volatile Data data = null;

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

Itt még mindig van zárolásunk, de csak akkor, ha adat == null. A fennmaradó eseteket volatile read segítségével szűrjük ki. A helyességet az biztosítja, hogy a volatile tároló megtörténik – a volatilis olvasás előtt, és a konstruktorban előforduló összes művelet látható annak, aki a mező értékét olvassa.