Ahogy a neve is mutatja, a singleton (magyarul egyke) egy példányban létezik a szűkebb világunkban. A szűkebb világ itt elsősorban a virtuális gépet jelenti, de alkalmazás szerverek környékén akár alkalmazás szintű is lehet ez a világ, ezért körültekintően használjuk: nem biztos, hogy két alkalmazásunk a singleton ugyanazon példányát használja.
A probléma
Sokszor fontos, hogy egy-egy osztályból csak egyetlen példány létezzen, gondoljunk például a programunk konfigurációs állományának beolvasására: tartalmát általában egyszer kell beolvasnunk, majd ezt fel kell dolgoznunk, aztán a program sok egyéb helyén használjuk a feldolgozott paramétereket. Ezen felül még több tucatnyi esetet lehet felsorolni, amikor egy osztályból csak egy példány létrehozása szükséges: különféle várakozósorok kezelése, egy példányból álló erőforrások kezelése, stb.
A megoldás
Megoldásképpen létrehozhatunk globális (public static) változókat valahol a programunkban, amelyekbe a program indulásakor értékül adjuk egy-egy példányát a szükséges osztályoknak - ez azonban lehetővé teszi, hogy más is példányosíthassa a felhasznált osztályokat.
Sokkal szebb megoldás erre a feladatra singleton osztály használata, amely annyiban különbözik a fenti esettől, hogy önmaga tartalmazza saját egyetlen példányát egy globális változóban, a konstruktora pedig nem érhető el külső osztály számára, így az első és egyetlen példány létrehozását is maga a singleton végzi.
Ha a felhasználók kérnek egy példányt a singleton osztályunk, akkor nem feltétlen kell tudniuk, hogy másokkal osztoznak ezen a példányon, ezért ha felmerül a gyanú, hogy több szálból is hívhatják a visszaadott példány metódusait, akkor úgy kell megírnunk ezeket, hogy azok szál biztosan működjenek.
Általánosságban a singleton osztályunkban biztosítani kell egy nem publikus konstruktort, amely létrehozza az egyetlen példányt, illetve léteznie kell egy publikus és statikus változónak/metódusnak, amely hordozza/visszaadja ezt az egyetlen példányt - vagyis önmagát.
Az implementáció
Felmerülhet a kérdés, hogy miért kell a singleton , amikor statikus osztályt is használhatnánk. A statikus osztály és a singleton között alapvetően az a különbség, hogy a statikus osztály minden statikus tagja létrejön akkor, amikor a classloader betölti az osztályt, a singleton csak attól a pillanattól foglal memóriát, amikor először használjuk azt. További különbség, hogy a statikus osztály nem implementálhat interfészt, míg a singleton igen, hiszen a Java szempontjából egy teljesen közönséges osztály. Nézzünk pár implementációt, amelyek más-más irányból közelítik meg a problémát: ugyan a végső eredmény azonos lesz, de az első példány létrehozása máskor és máshogy történik.
A klasszikus megoldás
Mivel a singleton lehetősége a Java egész korai verzióiban is rendelkezésre állt (hiszen ez - mint a legtöbb tervezési minta - egy OOP minta, nem a Java sajátja), ezért eleinte a klasszikus megoldást használhattuk:
public class Singleton { public static final Singleton INSTANCE = new Singleton(); protected Singleton() { // ... } }
Mint láthatjuk, itt a példány publikus, azaz nincs egy olyan metódus, amelyen keresztül elkérhetjük; ezen túl statikus, amely biztosítja az egyetlen példányt; s végül final, vagyis a tartalma nem módosítható többet. Az egyetlen probléma a korai példányosodás, ugyanis ez az implementáció a Singleton osztály betöltésekor létrehozza a példányát, ami nem mindig a leghasznosabb megoldás.
A lusta (lazy) Singleton
Kismértékben javíthatunk a kifacsarható teljesítményen, ha a singleton példány akkor jön létre, amikor először használnák azt fel:
public class LazySingleton { protected static LazySingleton INSTANCE; protected LazySingleton() { // ... } public static synchonized LazySingleton getInstance() { if (INSTANCE == null) { INSTANCE = new LazySingleton(); } return INSTANCE; } }
Ennek a megoldásnak a hátránya, hogy a getInstance metódust meg kell védenünk attól, hogy egyszerre több szál is benne tartózkodjon, amely kissé lassít az elsőt követő használatokon, hiszen ekkor már a létrehozott példányt szeretnénk megkapni, amely műveletet nem kellene szinkronizálni.
A javasolt megoldás
Bill Pugh felfedezett egy olyan megoldást, amely a lehető legkésőbb hozza létre az egyetlen példányt, és teljes mértékben szálbiztos:
public class OptimalSingleton { protected OptimalSingleton() { // ... } private static class SingletonHolder { private final static OptimalSingleton INSTANCE = new OptimalSingleton(); } public static OptimalSingleton getInstance() { return SingletonHolder.INSTANCE; } }
Ez a végletekig egyszerű és hatékony megoldás a virtuális gép működésére épít, ugyanis a classloader csak akkor tölti be a SingletonHolder osztályt, amikor valaki meghívja a getInstance metódust. A szálbiztosságot pedig az garantálja, hogy a classloader csak egyszer tudja betölteni a szükséges osztályt és ez JVM szintű atomi művelet.
Egyszerűen csak enum
A Java 5 által behozott enum osztálytípus új lehetőséget adott a programozók kezébe:
public enum EnumSingleton { INSTANCE; EnumSingleton() { // ... } }
A megoldás szálbiztos, de sajnos nem az első használatkor jön létre a példány, ellenben igen tömör megoldás. További előnye a többi megoldással szemben, hogy önmagában szerializálható, mivel az enum típusú osztályok erre automatikusan képesek.
A lusta (lazy) Singleton – Java 5 esetén
A Java 5 új memóriakezelést hozott a virtuális gépbe, ezért a volatile használatával lehetőségünk van az alábbi megoldásra:
public class VolatileSingleton { private static volatile VolatileSingleton INSTANCE; protected VolatileSingleton() { // ... } public static VolatileSingleton getInstance() { if (INSTANCE == null) { synchronized (VolatileSingleton.class) { if (INSTANCE == null) { INSTANCE = new VolatileSingleton(); } } } return INSTANCE; } }
A kétszeres ellenőrzés a példány lekérdezésének gyorsaságát biztosítja, a volatile pedig ügyel arra, hogy csak akkor legyen az INSTANCE értéke nem null, ha a konstruktor már lefutott.
Mérjünk!
Az öt implementáció különbözik egymástól lehetőségekben és futási időkben, különböztessünk meg futási időket első használatra és további használatra. A singleton konstruktorába tegyünk egy 1000ms idejű várakozást, és nézzük meg mennyi idő alatt hajtódik végre az első singleton példány elkérése, illetve mennyi példányt tudunk elkérni a további alkalmak során:
Módszer | Első példány | További példányok |
---|---|---|
ClassicSingleton | ~1000ms | > 10 millió / ms |
LazySingleton | ~1000ms | ~20 ezer / ms |
HolderSingleton | ~1000ms | > 10 millió / ms |
EnumSingleton | ~1000ms | > 10 millió / ms |
DoubleLazySingleton | ~1000ms | ~200 ezer / ms |
Mint látszik, az első példány elkérése mindenhol közel 1000ms körül van, a különbség akkor ütközne ki, ha a classloader az első felhasználás előtt töltené be a singleton osztályt, de nehézségbe ütközik előbb használni az osztályt, minthogy használnánk azt... :)
A további példányok elkérése során látszik, hogy azok a getInstance metódusok igen rosszul teljesítenek, amelyekben szinkronizációt használunk, ezért lehetőleg kerüljük ezeket, és próbáljuk meg a HolderSingleton mintát felhasználni, amely egyszerű, szálbiztos és gyors.
(forrás: wikipedia)
6 Comments
Böszörményi Péter
Erdemes megjegyezni azt, hogy a Singleton egyik mellekhatas, hogy globalis elerhetoseget biztosit, ami viszont majdnem olyan, mint a globalis valtozo volt. Ugyhogy esszel, mert a mertektelen fogyaztasa karositja a kornyezet egeszseget. Ha lehet, akkor kerulendo.
Auth Gábor
Mint ahogy írtam is, a singleton globális volta attól függ, hogy hány classloader van a JVM-ben és a singleton-t melyik tölti be: ugyanis csak a betöltő classloader ág alatt "globális".
Sándor Tamás
Volna egy kérdésem: miért akarunk egy singleton-t szerializálni (lásd singleton enum-mal)?
Holczhauser Károly
Szia Gábor !
Nagyon jók a minta leírások ! Mikorra várható újabb cikkek (azaz minták) megjelenése a témában?
Tóth Gergely
Sziasztok,
A Bill Pugh féle megoldással kapcsolatban van valami, amire érdemes odafigyelni:
Java-ban ha egy statikus inicializáló blokk RuntimeException-t vagy Error-t dob, akkor első alkalommal a hívófél egy ExceptionInInitializerError-t kap, amiben természetesen megtalálható causeként az eredeti kivétel vagy error. Ezzel szemben az összes többi alkalommal, amikor hivatkozunk arra az osztályra, aminek a statikus init blokja elszállt korábban, akkor NoClassDefFoundError-t kapunk anélkül, hogy a statikus init blokk lefutna. Ez feltehetően azért van így, mert a ExceptionInInitializerError-ra már meg kellett volna halnia az alkalmazásnak.
Ha megengedett olyan eset, amikor a singleton példányosítása sikertelen viszont később még lehetne sikeres, akkor ez a megoldás ahelyett, hogy létrehozná a példányt félrevezető hibákat fog dobálni.
Mindamellet, szerintem is ez a legelegánsabb megoldás a singletonra.
Keresztes József (xesj.hu)
Sziasztok !
Tóth Gergővel értek egyet. Ezekkel a statikus inicializátorokkal óvatosan kell bánni. Betelepítünk egy webalkalmazást, valami user hívja meg elsőként, nála még megvan a valódi exception, amikor mi hívjuk 3 másodperc múlva már csak NoClassDefFoundError van
Én mindig azt szoktam mondani hiába hasonlítgatjuk itt az időket, általában lényegtelen szempont ! Az alkalmazást úgyis az adatbázis elérés lassítja, mert mondjuk egy SQL-select 3 másodpercig fut. Lényegtelen a singleton megvalósítás. Én LazySingleton-t használok, sosem volt még vele baj, ennél 1000* nagyobb problémákkal találkozunk egy komplett alkalmazásnál.
Joe