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.
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.
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.
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.
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.
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.
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.
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 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.
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:
|
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)