Útmutató a Java illékony kulcsszavához

1. Áttekintés

Szükséges szinkronizálás hiányában a fordító, a futásidejű vagy a processzorok mindenféle optimalizálást alkalmazhatnak. Annak ellenére, hogy ezek az optimalizálások legtöbbször előnyösek, néha finom problémákat okozhatnak.

A gyorsítótár és az átrendezés azok közé az optimalizálások közé tartozik, amelyek meglephetnek minket egyidejű kontextusban. A Java és a JVM számos módot kínál a memória rendjének és a illó kulcsszó az egyik ilyen.

Ebben a cikkben erre a megalapozott, de gyakran félreértett koncepcióra fogunk összpontosítani a Java nyelven - a illó kulcsszó. Először egy kicsit áttekintjük az alapul szolgáló számítógép-architektúra működését, majd megismerkedünk a Java memóriarendjével.

2. Közös többprocesszoros architektúra

A processzorok felelősek a program utasításainak végrehajtásáért. Ezért be kell szerezniük a program utasításait és a szükséges adatokat a RAM-ból.

Mivel a CPU-k képesek jelentős számú utasítás végrehajtására másodpercenként, a RAM-ból való lehívás nem olyan ideális számukra. Ennek a helyzetnek a javítása érdekében a processzorok olyan trükköket alkalmaznak, mint Out of Order Execution, Branch Prediction, Spekulatív végrehajtás és természetesen Cache.

Itt jön létre a következő memóriahierarchia:

Mivel a különböző magok több utasítást hajtanak végre és több adatot manipulálnak, a gyorsítótárukat relevánsabb adatokkal és utasításokkal töltik fel. Ez javítani fogja az általános teljesítményt a gyorsítótár-koherencia kihívásainak rovására.

Leegyszerűsítve kétszer is át kell gondolnunk, mi történik, ha egy szál frissíti a gyorsítótárazott értéket.

3. Mikor kell használni illó

A gyorsítótár koherenciájának bővebb kibővítése érdekében kölcsönözzünk egy példát a Java Concurrency in Practice című könyvből:

public class TaskRunner {privát statikus int szám; saját statikus logikai készen áll; private static class Reader kiterjeszti a szálat {@Override public void run () {while (! ready) {Thread.yield (); } System.out.println (szám); }} public static void main (String [] érvel) {new Reader (). start (); szám = 42; kész = igaz; }}

A TaskRunner osztály két egyszerű változót tart fenn. Fő módszerében létrehoz egy másik szálat, amely forog a kész változó, amíg van hamis. Amikor a változó válik igaz, a szál egyszerűen kinyomtatja a szám változó.

Sokan számíthatnak arra, hogy a program rövid késés után egyszerűen kinyomtatja a 42-et. A valóságban azonban a késés sokkal hosszabb lehet. Lehet, hogy örökké lóg, vagy akár nullát is nyomtathat!

Ezeknek a rendellenességeknek az oka a memória megfelelő láthatóságának és átrendezésének hiánya. Értékeljük őket részletesebben.

3.1. Memória láthatósága

Ebben az egyszerű példában két alkalmazásszálunk van: a fő és az olvasó szál. Képzeljünk el egy forgatókönyvet, amelyben az operációs rendszer két különböző CPU-magra ütemezi ezeket a szálakat, ahol:

  • A fő szálnak megvan a másolata kész és szám változók az alapvető gyorsítótárában
  • Az olvasószál a másolataival is végződik
  • A fő szál frissíti a gyorsítótárazott értékeket

A legtöbb modern processzoron az írási kérelmek a kiadásuk után nem lesznek azonnal alkalmazhatók. Valójában, a processzorok hajlamosak egy speciális írási pufferbe állítani ezeket az írásokat. Egy idő után ezeket az írásokat egyszerre alkalmazzák a fő memóriában.

Mindezzel együtt, amikor a fő szál frissíti a szám és kész változók, nincs garancia arra, hogy mit láthat az olvasószál. Más szavakkal, az olvasó szála azonnal, némi késéssel, vagy soha nem láthatja a frissített értéket!

Ez a memória láthatóság életképességi problémákat okozhat a láthatóságra támaszkodó programokban.

3.2. Újrarendelés

Hogy még rosszabb legyen a helyzet, az olvasó szál láthatja ezeket az írásokat a tényleges program sorrendjétől eltérő sorrendben. Például, mivel először frissítettük a szám változó:

public static void main (String [] args) {new Reader (). start (); szám = 42; kész = igaz; }

Várhatjuk, hogy az olvasószál nyomtatása 42. Valójában azonban nullát lehet látni nyomtatott értékként!

Az átrendezés egy optimalizálási technika a teljesítmény javításához. Érdekes módon különböző összetevők alkalmazhatják ezt az optimalizálást:

  • A processzor az írási puffert a program sorrendjétől eltérő sorrendben öblítheti át
  • A processzor rendelésen kívüli végrehajtási technikát alkalmazhat
  • A JIT fordító optimalizálhatja az átrendezést

3.3. illó Memória rend

Annak biztosítására, hogy a változók frissítései kiszámíthatóan terjedjenek más szálakra, alkalmaznunk kell a illó módosítója ezeknek a változóknak:

public class TaskRunner {private volatile static int number; privát illékony statikus logikai készen áll; // ugyanaz, mint korábban}

Így kommunikálunk a futási idővel és a processzorral, hogy ne rendezzünk át semmilyen utasítást, amely a illó változó. A processzorok megértik, hogy ezeknek a változóknak a frissítéseit azonnal ki kell üríteniük.

4. illó és a szál szinkronizálása

A többszálú alkalmazásoknál pár szabályt kell biztosítanunk a következetes viselkedéshez:

  • Kölcsönös kizárás - egyszerre csak egy szál hajt végre kritikus szakaszt
  • Láthatóság - az egyik szál által a megosztott adatokban végrehajtott változtatások láthatók a többi szál számára is az adatok konzisztenciájának fenntartása érdekében

szinkronizált a módszerek és a blokkok biztosítják a fenti tulajdonságokat, az alkalmazás teljesítményének árán.

illó nagyon hasznos kulcsszó, mert az segíthet az adatok változásának láthatósági szempontjának biztosításában, természetesen kölcsönös kizárás biztosítása nélkül. Így hasznos azokon a helyeken, ahol rendben vagyunk, ha több szál párhuzamosan végrehajt egy kódblokkot, de biztosítanunk kell a láthatósági tulajdonságot.

5. Történik-megrendelés előtt

A memória láthatóságának hatásai illó változók túlmutatnak a illó maguk a változók.

A dolgok konkrétabbá tétele érdekében tegyük fel, hogy az A szál ír a illó változó, majd a B szál ugyanezt olvassa illó változó. Ilyen esetekben, azokat az értékeket, amelyek A számára láthatóak voltak a illó változó B számára látható lesz a illó változó:

Technikailag szólva bármilyen írás a illó mező ugyanazon mező minden következő olvasása előtt történik. Ez a illó a Java memóriamodell (JMM) változó szabálya.

5.1. Piggybacking

A történelem előtti memória-rendezés erőssége miatt néha visszacsatolhatunk egy másik láthatósági tulajdonságaira illó változó. Például a konkrét példánkban csak meg kell jelölnünk a kész változó as illó:

public class TaskRunner {privát statikus int szám; // nem volatile private volatile static boolean ready; // ugyanaz, mint korábban}

Bármi az írás előtt igaz hoz kész változó bárki számára látható a kész változó. Ezért a szám változó visszacsatolás a memória láthatóságára, amelyet a kész változó. Egyszerűen fogalmazva, annak ellenére, hogy nem a illó változó, a illó viselkedés.

Ezen szemantika felhasználásával osztályunkban csak néhány változót határozhatunk meg illó és optimalizálja a láthatósági garanciát.

6. Következtetés

Ebben az oktatóanyagban többet tártunk fel a illó kulcsszó és képességei, valamint a Java 5-tel kezdődő fejlesztések.

Mint mindig, a kód példák megtalálhatók a GitHub oldalon.