Wzorce projektowe w programowaniu – wzorzec Singleton

Singleton – do czego się go stosuje

Wzorzec projektowy Singleton stosowany jest wszędzie tam, gdzie wymagane jest stworzenie jednego, i tylko jednego, egzemplarza danego obiektu. Przykładem takiej sytuacji np. okno dialogowe, sterowniki drukarek czy obiekty wykorzystywane podczas procesu logowania. Można oczywiście stworzyć jeden egzemplarz obiektu poprzez zmienne globalne, jednak ma to swoje słabe strony – w momencie uruchomienia aplikacji trzeba stworzyć instancję obiektu. Jeżeli taki obiekt zużywa dużo pamięci systemowej i nie zajdzie zdarzenie, które będzie powodować wykorzystanie tego obiektu, to tracimy wiele zasobów.

Przeanalizujmy sobie parę rzeczy:
– nowy obiekt tworzymy poprzez: new NowyObiekt(),
– jeżeli inny obiekt także chciałby stworzyć NowyObiekt() wystarczy, że użyje operatora new,
– tak długo, jak długo mamy daną klasę, i jest ona publiczna, możemy zawsze utworzyć jeden lub kilka obiektów tej klasy,
– jeżeli klasa nie jest publiczna, to obiekt tej klasy (w dowolnych ilościach) mogą tworzyć tylko i wyłącznie klasy z tego samego pakietu.
Możemy jednak spowodować, że dana instancja obiektu zostanie utworzona tylko raz. Spójrzmy na poniższy kod.

public NowaKlasa() {
     private NowaKlasa();
     
     public static NowaKlasa pobierzInstacje() {
          return new NowaKlasa();
     }
}

Konstruktor tej klasy jest prywatny. Ponadto, zaimplementowana jest metoda statyczna, wywoływana poprzez NowaKlasa.pobierzInstancje(). Dodajmy do tego warunek, który uniemożliwi stworzenie więcej niż jednego obiektu tejże klasy.

public class NowaKlasa() {
     private static NowaKlasa unikalnaInstancja;

     private NowaKlasa();

     public static NowaKlasa pobierzInstacje() {
          if (unikalnaInstancja == null) {
               unikalnaInstancja = new NowaKlasa();
          }
          return unikalnaInstancja;
     }    
}

Jako, że konstruktor klasy jest prywatny, to tylko obiekt klasy NowaKlasa może z niego korzystać i utworzyć nową instancję obiektu tej klasy.

Definicja wzorca Singleton

Znamy już podstawę działania wzorca, więc spójrzmy na regułę.

Wzorzec Singleton zapewnia, że dana klasa będzie miała tylko i wyłącznie jedną instancję obiektu, i zapewnia globalny punkt dostępu do tej instancji.

Opisując to inaczej, bierzemy daną klasę i pozwalamy jej zarządzać pojedynczą instancją jej obiektu. Zapobiegamy również utworzeniu nowej instancji takiego obiektu przez inne klasy. Aby otrzymać jego instancję, musimy posłużyć się jego klasą macierzystą. Zapewniamy także jeden, globalny punkt dostępu do instancji tego obiektu: gdy jest on potrzebny, zwracamy się do jego klasy macierzystej a ona daje nam jego instancję.

Zobaczmy, jak wygląda diagram wzorca Singleton.

Zmienna klasowa unikalnaInstancja przechowuje naszą jedyną instancję obiektu NowaKlasa.

Metoda pobierzInstancje() jest metodą statyczną (klasową). Dzięki temu mamy do niej wygodny dostęp z dowolnego miejsca aplikacji poprzez wyrażenie NowaKlasa.pobierzInstancje(). Jest to rowiązanie przypominające dostęp do zmiennych globalnych, ale dodatkowo posiadające te zaletę, ze możemy wykorzystać takie mechanizmy wzorca Singleton, jak opóźnione tworzenie instancji obiektu.

Problem przy wprowadzeniu wielowątkowości

Może się jednak okazać, że podczas zastosowania powyższego kodu wzorca Singleton i dodaniu do naszej aplikacji wielowątkowości dojdzie do sytuacji, kiedy metoda pobierzInstancje() zostanie wywołana w tym samym czasie dwa lub więcej razy. Wtedy warunek nieistniejącego obiektu zostanie spełniony w każdej metodzie, a to oznacza, że każda utworzy osobny obiekt. Jednym z rozwiązań tego problemu jest dodanie do metody pobierzInstancje() modyfikatora „synchronized”.

public class NowaKlasa() {
     private static NowaKlasa unikalnaInstancja;

     private NowaKlasa();

     public static synchronized NowaKlasa pobierzInstacje() {
          if (unikalnaInstancja == null) {
               unikalnaInstancja = new NowaKlasa();
          }
          return unikalnaInstancja;
     }    
}

Poprzez dodanie modyfikatora „synchronized” zmuszamy poszczególne wątki do oczekiwania na swoją kolej w dostępie do tej metody. Dzięki temu tylko jeden wątek ma w jednym momencie dostęp do tej metody. Wtedy, gdy metoda ta zwróci obiekt, warunek istnienia obiektu będzie już spełniony za każdym kolejnym razem.

Modyfikator „synchronized” – wady i rozwiązania zamienne

O ile dodanie modyfikatora „synchronized” spełnia swoją funkcję, to synchronizacja czasów dostępu do metody może nawet stukrotnie zmniejszyć wydajność działania aplikacji. Może, lecz nie musi – zależy to od tego, czy metoda zwracająca obiekt używana jest wiele razy.
Ponadto, synchronizacja ta jest niezbędna tylko przy pierwszym użyciu tej metody. Kiedy stworzymy instancję naszego obiektu i zapiszemy ją w zmiennej unikalnaInstancja, nie ma potrzeby stosowania omawianego modyfikatora. Co zatem zrobić?

Pierwszym rozwiązaniem jest nie robienie nic i zostawienie modyfikatora na swoim miejscu. Jeżeli wywoływanie synchronizowanej metody pobierającej instancję nie spowalnia w drastyczny sposób aplikacji, to dobrze jest pozostanie przy dotychczasowym, prostym rozwiązaniu.

Drugim rozwiązaniem jest zmodyfikowanie kodu w taki sposób, aby obiekt został utworzony z wyprzedzeniem. Jeżeli aplikacja zawsze będzie korzystać z tego jedynego obiektu (w naszym przypadku obiektu klasy NowaKlasa), lub też wczesne utworzenie tego obiektu nie będzie stanowiło znaczącego obciążenia można postąpić tak jak poniżej.

public class NowaKlasa() {
     private static NowaKlasa unikalnaInstancja = new NowaKlasa();

     private NowaKlasa();

     public static synchronized NowaKlasa pobierzInstacje() {
          return unikalnaInstancja;
     }    
}

Tworzymy odpowiednią instancję w statycznej metodzie inicjalizacyjnej. Kreację obiektu powierzamy wirtualnej maszynie, która dokonuje tego zaraz po załadowaniu klasy – na długo przed tym, zanim jakikolwiek watek będzie usiłował uzyskać dostęp do statycznej zmiennej unikalnaInstacja.

Trzecim rozwiązaniem jest zastosowanie metody „podwójnego blokowania” (ang. double-checked locking) do zredukowania niezbędnych synchronizacji wykonania metody pobierzInstacje(). Korzystając z takiej metody najpierw sprawdzamy, czy instancja obiektu została już utworzona. Jeżeli nie, to wtedy uruchamiamy synchronizacje metody. W ten sposób korzystamy z synchronizacji tylko raz, podczas tworzenia obiektu.

public class NowaKlasa() {
     private volatile static NowaKlasa unikalnaInstancja;

     private NowaKlasa();

     public static NowaKlasa pobierzInstacje() {
          if (unikalnaInstancja == null) {
               synchronized (NowaKlasa.class) {
                    if (unikalnaInstancja == null) {
                         unikalnaInstancja = new NowaKlasa();
                   }
               }
          }
          return unikalnaInstancja;
     }    
}

Zastosowanie słowa kluczowego „volatile” powoduje, że zmienna unikalnaInstancja jest prawidłowo traktowana w środowisku wielowątkowym, kiedy następuje przypisanie do niej nowo utworzonej instancji obiektu.
Jednak metoda „podwójnego blokowania” nie działa na platformie Java 1.4 i starszych.