Child pages
  • Singleton
Skip to end of metadata
Go to start of metadata

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:

Singleton.java
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:

LazySingleton.java
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:

OptimalSingleton.java
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:

EnumSingleton.java
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:

VolatileSingleton.java
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ódszerElső példányTová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)

 

      
      
Page viewed times

6 Comments

  1. 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.

    1. 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".

  2. Volna egy kérdésem: miért akarunk egy singleton-t szerializálni (lásd singleton enum-mal)?

  3. 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?

  4. 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.

  5. 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 (smile) É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