tifyty

pure Java, what else ?

Egyke

Mostanában sokszor beszélgetek fiatal kollégákkal, és fel szokott merülni a tervezési minta kérdése. Ilyenkor szinte mindig előbújik az egyke.

Ha nem lenne meg, hogy miről is beszélek, akkor leánynevén singleton-nak is szólíthatjuk. Volt olyan beszélgető társam is, aki csak az “egyke” szót ismerte a singleton-t nem. No, de vissza az eredeti témára.

Miről ismerszik meg az egyke? Hogy egy van belőle. Hmmm… Mi garantálja, hogy egy távoli univerzumban, egy párhuzamos világban nincs belőle még egy példány? Vajon hányan tudják, hogy class loaderenként lehet belőle egy.

És hogyan néz ki egy tipikus egyke osztály?

public class Singleton {
	private static Singleton instance = new Singleton();

	public static Singleton getInstance() {
		return instance;
	}
}

Ennél egyszerűbb már nem is lehet. Sok mindenre nem is jó, maximum, hogy szinkronizáláshoz felhasználjuk.

És mi van akkor, ha nem minden lefutásnál kell ez az egyke, és a létrehozása nem olyan egyszerű, mint a példában. Tegyük fel, hogy a programot sokszor indítjuk el, és csak néha van szükség a singleton-ra, és a létrehozása pedig hosszú másodperceket igényel. Nem lehetne, hogy csak akkor hozzuk létre, ha szükségünk van rá? Miért kell már az osztály betöltése során létrehozni a példányt?

public class Singleton {
	private static Singleton instance = null;
	public static Singleton getInstance() {
		if( instance == null ){
			instance = new Singleton();
		}
		return instance;
	}
}

Szuper! We happy, Vince!

Vagy nem vagyunk boldog, Vince?

Háát, de… de csak amíg a szálak össze nem érnek, és nem kezdenek makramézni a JVM-ben. Akkor aztán történhetnek olyan kesze-kuszaságok, hogy Alexander legyen a talpán aki kibogozza. Mert mi történik akkor, ha két szál egyszerre (kvázi egyszerre, vagy több processzor esetében ténylegesen) kívánja meg azt az egykét? Mind a kettőnek kell, de egy sincs. Még. És ezt a tényt mind a kettő látja, hiszen

		if( instance == null ){

bizony mind a kettő számára igaz lesz. És mind a kettő létrehoz egy példányt, és mind a kettő azt fogja hinni, hogy az az egy példány az igazi, az egyetlen, és aztán, amikor az operában szembejön a másik, és ugyanaz az egyedi singleton kosztüm van rajta, akkor megüti a guta. Persze Java-ban nem üti meg a guta, mert ez nem C és nincs “memory fault core dumped”. Van helyette más.

Hát akkor ha már több szálon futtatjuk a dolgokat, akkor erre a célra ott a synchronized:

public class Singleton {
	private static Singleton instance = null;
	public static synchronized Singleton getInstance() {
		if( instance == null ){
			instance = new Singleton();
		}
		return instance;
	}
}

Na de most már aztán tényleg boldogok vagyunk! Szétvágtuk a gordiuszi csomót. Mondjuk nem ártott volna kibogozni, mert így nincs elég madzagunk, de makramézás az nincs. Van helyette várakozás.

Akkor, amikor mind a 4000 szál meg akarja szerezni azt az ezer éve létrehozott singleton-t, már rég el lehetne felejteni a létrehozás körüli versenyhelyzetet, de ehelyett a szinkronizált getInstance metóduson éheznek a szálak. Ahelyett, hogy minden szál megkaphatná a saját kis referenciáját, egymás után szolgáljuk ki őket, mint az ingyenkonyhán a szegényeket.

Ekkor jön a csodafegyver a DCL, azaz Double Check Locking tervezési minta, ami mintának nem rossz, de speciel a Java implementációban nem működik. Illetve néha működik. Azaz, hogy egészen pontos legyek: majdnem mindig működik. Csak kozmikus valószínűtlenségi esetekben nem, de amikor az bekövetkezik, akkor bajt okoz, és ha nem tudjuk, hogy mi okozta, akkor csak nézünk, mint bárány a nyársra/nyárson, mert nem tudjuk a hibát reprodukálni. Na de mi is ez? Nézzük meg a kódot:

public class Singleton {
	private static Singleton instance = null;
	public static Singleton getInstance() {
		if( instance == null ){
			synchronized (Singleton.class) {
				if( instance == null ){
					instance = new Singleton();					
				}
			}
		}
		return instance;
	}
}

Ez a kód azt mondja, hogy nézzük meg, hogy null-é a singleton instance, és ha igen, akkor hozzuk létre. De ha neki is állunk létrehozni, akkor is csak akkor tegyük meg tényleg, ha most már szinkronizáltan csak mi férünk hozzá, és még mindig nem hozta létre senki más. Előfordulhat ugyan, hogy az instance null volt, ám miközben arra vártunk, hogy más ne matassa és létrehozhassuk valaki már létrehozta. Ebben az esetben boldogan használjuk a létrejött példányt. Ha viszont senki más nem hozta létre, még most sem, hogy a saját szálunk átjutott az első null vizsgálaton, majd a synchronized blokkba is bejutott, és sikeresen tovább haladt a második null vizsgálaton, akkor és csak akkor biztosak lehetünk abban, hogy mi vagyunk az egyetlen jogosult szál, aki a singleton objektumot létrehozhatja, és létre is hozzuk. És ez igaz is, a pattern a Java esetében ugyanis nem itt törött. Azt a problémát, amit az első esetben a szinkronizálás hiánya okozott, és ami miatt több példány is létrejött (tudod, kiskosztüm, opera, migrén, hiszti), nos ezt megoldottuk. De ezt már megoldottuk a getInstance metódus szinkronizálásával is, csak attól nagyon lelassult az egész. Máshol van a probléma.

A probléma ott van, hogy nem jól gyorsítjuk fel a hozzáférést. A szálak nem lépnek bele semmilyen szinkronizált blokkba, amikor hozzá akarnak férni a példányhoz, ezért aztán nem is lassú a hozzáférés, de nem is biztonságos.

Nézzünk egy egyszerű példát:

  • Egyik szál látja, hogy null az instance.
  • Egyik szál belefut a synchronized blokkba.
  • Egyik szál látja, hogy még mindig null az instance.
  • Egyik szál elkezdi létrehozni az új instance-t.
  • És ekkor, pontosan ekkor megjelenik egy másik szál, és azt látja, hogy az instance már nem null, és elkezdi használni a még létre sem hozott singleton objektumot.

Na várjunk csak! Miért is nem null? Most akkor nem hozta még létre, és akkor null, vagy nem null és akkor létrehozta. Vagy nem?

Hát nem. A Java nem garantálja, hogy a generált byte kód, vagy a JIT által generált kód olyan sorrendben hajtja végre az elemi műveleteket, ahogy azt mi elképzeljük. Mi is történik nagy vonalakban a

					instance = new Singleton();					

sor végrehajtása során?

  1. A JVM az édenből lefoglal egy akkora memória területet, amekkora a Singleton objektumnak kell.
  2. A JVM inicializálja az objektumot. (Lefuttatja a konsruktort, meg még néhány dolgot.)
  3. A JVM az új objektum referenciáját (memória cím, pointer) betölti az instance változóba.

Valójában a Java csak azt garantálja, hogy az 1 és 2 lefut, és amikor az utasítás véget ért, akkor az instance változóban benne lesz a referencia értéke. Hogy az mikor kerül oda, az az ő dolga. És ha már itt tartunk, akkor az is érdekes kérdés, hogy mit jelent, az “oda”. Hol tárolja a program az instance változót? Memóriában? Processzor cache-ben? Processzor regiszterben? Mikor éppen hol, ahol és ahogy a program futásának az a legkedvezőbb, és ahogy az a JIT kioptimalizálja? Bizony ez utóbbi az igaz. Amit tudni lehet, hogy amikor belefut egy synchronized blokkba, és amikor kijön onnan, akkor minden változót, ami a program számára a blokkban kell, illetve amit ott módosított beolvas és kiír memóriába/ból. Tehát a többi thread valamikor, előbb, vagy utóbb, esetleg évezredek múlva, feltehetőleg az instance változó értékét fel fogja olvasni a memóriából a saját processzorába, amelyik éppen őt futtatja. De erre persze garancia nincs, hiszen ezek a thread-ek nem futnak bele a synchronized blokkba.

Persze a konkrét esetben egy thread vagy beolvassa memóriából, hiszen nem volt meg neki, és honnan venné a nem null értéket, vagy ha null van a memóriában, akkor bele fog futni a synchonized blokkba. De ez sem vigasztal minket. Az történhet ugyanis, hogy a fenti három műveletet a JIT által generált natív kód, az 1,3,2 sorrendben hajtja végre. Azaz

  1. Lefoglalja a memória területet.
  2. A referenciát beteszi az instance változóba
  3. Végrehajtja az objektum inicializálását.

Ekkor bizony előfordulhat, hogy egy konkurens szál az instance változóban már nem null-t lát, és de az általa hivatkozott objektum még nincs teljesen kész.

Elvileg. Meg ha a nap és a hold együttállása olyan. Meg a Vénusz is. De ha a legkisebb lehetősége is van annak, hogy a kód eltörjön, akkor az el fog törni. És ha lehetősége van, hogy ne törjön el debug környezetben, akkor ott nem fog eltörni. (Ezért kell minden éles kódot debug környezetben futtatni Murphy szerint.)

Mi akkor a megoldás?

public class Perezoso {
	private static class Nino {
		private static Perezoso instance = new Perezoso();
	}
	public static Perezoso getInstance() {
		return Nino.instance;
	}
}

Hát ez.
UPDATE
Elég sok visszajelzés érkezett, és ezek közül a legfontosabbak:

  • Singleton-t létre lehet hozni Java enum-ként is, ami “by default” Singleton.
  • A DCL megoldás Java 1.5 és későbbi verziókban működik, mert megváltozott a memória modell, de ehhez az is szükséges, hogy az instance változó volatile legyen. E miatt, és egyéb hasonló dolgok miatt viszont lassú, ami jól látható
  • a JavaFórum korábbi cikkéből is.

18 responses to “Egyke

  1. b0c1 július 9, 2012 11:31 de.

    De miert nem enum? 🙂

  2. kivancsi július 16, 2012 5:24 de.

    Mi az első és az utolsó között a különbség (konkrétan) működésben?

    • v július 16, 2012 7:07 de.

      Az első létrehozza a singleton példányt, amikor betöltődik a Singleton osztály. Az utolsó viszont csak akkor hozza létre a példányt, amikor a Nino osztály töltődik be. Az viszont csak akkor töltődik be, amikor a Perezoso osztály static getInstance metódusát meghívjuk.

  3. z július 26, 2012 10:28 de.

    …és ami egy óriási hiányossága a fenti leírásnak és példáknak, hogy a singletonnak private (de legalábbis egy) protected konstruktora kell, hogy legyen.

    • v július 26, 2012 10:41 de.

      Megtennéd, hogy pár mondatban leírod azt is, hogy miért?

      • z július 26, 2012 4:19 du.

        egyszerű. mert máskülönben new operátorral simán létrehozhatsz további instance-okat is az adott osztályból, mint amit a getInstance() metódus rendre visszaad. onnantól pedig nem lenne singleton a singleton az adott classloader alatt.

        • v július 26, 2012 4:43 du.

          A singletonnak, minden pattern-nek van egy megvalósítása, és hogy azt hogyan kell használni. A singleton esetében fontos, hogy ne hozzunk belőle létre más módon példányt, mint a getInstance statikus metódus meghívásával. Hogyan lehet ezt biztosítani?

          Az első lehetőség, hogy leírom a dokumentációban, hogy ne tedd. Ez valóban elég gyenge megoldás, no nem azért, mert ki lehet játszani, hanem azért, mert könnyen el lehet téveszteni, és olyan kódot írni, ahol mégiscsak létrehozunk több példányt.

          A következő, amit te mondasz, a legelterjedtebb, hogy privát-tá teszem a konstruktort. Persze ebben az esetben is létre lehet hozni több példányt, de véletlenül már nem: reflection-nel, szánt szándékkal keresztül kell húzni a singleton implementáció szándékát. Ez a védelem sem tökéletes.

          Még tovább lehet menni, mint ahogy anno az unsafe package készítésénél tették a SUN mérnökei, és le lehet ellenőrizni a stack trace-ből, hogy honnan hívták meg a privát konstruktort. És ezt is meg lehet kerülni…

          Óriási hiányosság…

      • z július 26, 2012 4:39 du.

        ja, protectected láthatóságot (a private helyett) meg azért szoktuk megengedni, mert előfordulhat, hogy a singleton “designed for extension”. és így legalább subclass-olható. persze ennek veszélye van, mert az extend-áló összekutyulhatja az ősosztály jól megtervezett dolgait. de hát nem ez az első dolog, ami nem dizájnolható meg szépen java-ban.

      • z július 26, 2012 5:05 du.

        hacker szagot érzek. 🙂

        szóval a singleton, mint design patternnek az egyik ALAP kritériuma a private konstruktor. és hiányának súlyossága nem vita tárgya. ez nem a “legelterjedtebb lehetőség”, amit én említek, hanem egy olyan dolog, amire MINDENKI gondol, ha azt a szót mondom, hogy singleton. ergo egy contract.

        másrészt, ha terveztél már (publikus) api-t, akkor bizonyára egyet értesz, hogy a dizájn szó nem azt jelenti, hogy összehányok egy rakat osztályt, és remélem, hogy azt jól fogják majd használni. azaz badarság arra alapozni, hogyha egy osztálynak singleton a neve, akkor senki nem próbálja meg példányosítani az osztályt konstruktorral. (pl. vegyünk egy junior fejlesztőt 🙂 ) vagyis a jó dizájn NEM AZT JELENTI, hogy nem lehet a dolgokat meghackelni (pl. reflectionnel), hanem azt, hogy minimalizálod az api-d jóindulatú(!) de rossz felhasználásból fakadó hibák lehetőségét.

        Reflection: mivel ilyet nem használunk (vagy pedig csak nagyon-nagyon indokolt esetben, pl. proxy) normálisan dizájnolt kódban, ezért erre csak azt mondom, hogy a reflection túlzott használata csúnya hackelés.

      • z július 26, 2012 5:15 du.

        Re. július 26, 2012 – 4:45 du.

        maga a singleton elgondolás szerintem nem ördögtől való. inkább az a probláma, hogy a java milyen eszközöket biztosít számunkra a megalkotásához. egy singletonért gyakorlatilag picit fel kell áldoznunk a józan ész diktálta dizájn elveket. és sajna elég sok ilyen van a java-ban.

  4. z július 26, 2012 4:22 du.

    amúgy ez a singleton téma egy nagyon érdekes terület. megérne a singletonok deserializációja is egy rövid megemlékezést.

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s

%d blogger ezt kedveli: