1. Definicja
Singleton to kreacyjny wzorzec projektowy, którego celem jest zapewnienie, że w całej aplikacji istnieje tylko jedna instancja danej klasy oraz udostępnienie globalnego punktu dostępu do tego obiektu
Najprościej rzecz ujmując: jeden obiekt = jeden egzemplarz na całą aplikację.
Wzorzec ten należy do grupy wzorców kreacyjnych (ang. creational patterns) czyli takich, które zajmują się sposobem tworzenia obiektów. Singleton jest prawdopodobnie najczęściej rozpoznawanym wzorcem z całej rodziny GoF (Gang of Four), a jednocześnie jednym z tych, które najłatwiej nadużyć.
2. Przykład z życia: zobaczmy jak to zrozumieć?
Wyobraźmy sobie hotel, w którym mieszka kilkuset gości. Jedni chcą odebrać klucz do pokoju, inni zgłaszają awarię klimatyzacji, jeszcze inni pytają o godzinę śniadania.
Niezależnie od sprawy wszyscy kierują się do tego samego miejsca, czyli recepcji.
Recepcja:
- przechowuje informacje o rezerwacjach,
- wydaje klucze do pokoi,
- przyjmuje zgłoszenia od gości,
- udziela informacji o hotelu.
Nie tworzymy osobnej recepcji dla każdego gościa. Gdyby tak było, informacje o wolnych pokojach, rezerwacjach i gościach byłyby rozproszone w wielu miejscach, co szybko doprowadziłoby do chaosu.
W praktyce istnieje jedna recepcja, która stanowi centralny punkt zarządzania informacjami.
Podobnie działa Singleton.
Wyobraźmy sobie, że wiele różnych klas w aplikacji potrzebuje dostępu do bazy danych. Nie chcemy, aby każda z nich tworzyła własne połączenie i niezależnie zarządzała komunikacją z bazą.
Zamiast tego tworzymy jeden obiekt odpowiedzialny za dostęp do bazy danych i udostępniamy go wszystkim zainteresowanym elementom systemu.
Tak jak wszyscy goście korzystają z jednej recepcji, tak różne części programu korzystają z jednej instancji obiektu.
To właśnie jest idea Singletona jedna instancja dostępna dla wszystkich.
3. Przykład — naruszenie (bez Singletona)
Wyobraźmy sobie, że tworzymy system logowania zdarzeń dla aplikacji e-commerce. Każdy komponent serwis zamówień, serwis płatności, serwis wysyłki tworzy własny logger.
// Logger bez Singletona — każdy tworzy swoją instancję
class Logger {
private List<String> logi = new ArrayList<>();
// Publiczny konstruktor pozwala tworzyć dowolną liczbę instancji
public Logger() {
System.out.println("Tworzę nowy Logger...");
}
public void log(String wiadomosc) {
logi.add(wiadomosc);
System.out.println("[LOG] " + wiadomosc);
}
public List<String> pobierzLogi() {
return logi;
}
}
class OrderService {
// Każdy serwis tworzy własny Logger — zupełnie niezależny obiekt
private Logger logger = new Logger();
public void zlozZamowienie(String produkt) {
logger.log("Złożono zamówienie: " + produkt);
}
}
class PaymentService {
// Kolejna oddzielna instancja Loggera — inna lista logów!
private Logger logger = new Logger();
public void przetworzPlatnosc(double kwota) {
logger.log("Przetworzono płatność: " + kwota + " PLN");
}
}
public class Main {
public static void main(String[] args) {
OrderService orderService = new OrderService();
PaymentService paymentService = new PaymentService();
orderService.zlozZamowienie("Laptop");
paymentService.przetworzPlatnosc(3499.99);
// Próbujemy zebrać wszystkie logi — ale każdy serwis ma swoje!
// Nie istnieje żaden wspólny rejestr zdarzeń
// Mamy dwa oddzielne obiekty Logger, dwie oddzielne listy logów
// Niemożliwe jest zebranie pełnej historii zdarzeń w jednym miejscu
}
}
Co jest tutaj złe?
Każdy serwis działa na swoim prywatnym Loggerze. Logi z zamówień istnieją oddzielnie od logów płatności, nie ma jednego, spójnego rejestru zdarzeń. Gdybyśmy chcieli sprawdzić pełną ścieżkę konkretnego zamówienia od złożenia do płatności, musielibyśmy agregować dane z wielu niezależnych obiektów.
Konkretne konsekwencje:
- brak spójności stanu — każda instancja ma własną, izolowaną listę logów,
- niemożliwa agregacja — nie da się zebrać pełnej historii zdarzeń,
- marnotrawstwo zasobów — przy połączeniach z bazą danych oznacza to setki otwartych socketów,
- trudność w testowaniu — nie wiadomo, która instancja powinna być weryfikowana.
4. Przykład — zgodny ze wzorcem Singleton
Podejdźmy do tego lepiej. Chcemy, aby istniał jeden i tylko jeden Logger, dostępny dla całej aplikacji.
Wersja klasyczna (thread-unsafe — dla zrozumienia idei)
class Logger {
// Statyczne pole przechowujące jedyną instancję klasy
// null oznacza, że instancja jeszcze nie została utworzona
private static Logger instancja = null;
// Lista logów — wspólna dla całej aplikacji
private List<String> logi = new ArrayList<>();
// Prywatny konstruktor — nikt z zewnątrz nie może wywołać new Logger()
private Logger() {
System.out.println("Logger zainicjalizowany — tylko raz!");
}
// Statyczna metoda fabrykująca — jedyny sposób na uzyskanie instancji
public static Logger pobierzInstancje() {
if (instancja == null) {
// Tworzymy instancję tylko wtedy, gdy jeszcze nie istnieje
instancja = new Logger();
}
return instancja;
}
public void log(String wiadomosc) {
logi.add(wiadomosc);
System.out.println("[LOG] " + wiadomosc);
}
public List<String> pobierzLogi() {
return Collections.unmodifiableList(logi);
}
}
class OrderService {
public void zlozZamowienie(String produkt) {
// Nie tworzymy nowego Loggera — pobieramy jedyną istniejącą instancję
Logger.pobierzInstancje().log("Złożono zamówienie: " + produkt);
}
}
class PaymentService {
public void przetworzPlatnosc(double kwota) {
// Ta sama instancja co w OrderService — ten sam rejestr logów
Logger.pobierzInstancje().log("Przetworzono płatność: " + kwota + " PLN");
}
}
public class Main {
public static void main(String[] args) {
OrderService orderService = new OrderService();
PaymentService paymentService = new PaymentService();
orderService.zlozZamowienie("Laptop");
paymentService.przetworzPlatnosc(3499.99);
// Teraz możemy zebrać WSZYSTKIE logi z jednego miejsca
List<String> wszystkieLogi = Logger.pobierzInstancje().pobierzLogi();
System.out.println("Historia zdarzeń: " + wszystkieLogi);
// → [Złożono zamówienie: Laptop, Przetworzono płatność: 3499.99 PLN]
}
}
Wersja bezpieczna dla wielu wątków (thread-safe)
W rzeczywistych aplikacjach wiele wątków może jednocześnie wywołać pobierzInstancje(). Wersja klasyczna ma tu lukę —
dwa wątki mogą jednocześnie przejść przez warunek if (instancja == null) i stworzyć dwie instancje. Rozwiązaniem jest
technika double-checked locking:
class Logger {
// volatile gwarantuje, że zapis do pola będzie widoczny dla wszystkich wątków
private static volatile Logger instancja = null;
private List<String> logi = new ArrayList<>();
private Logger() {}
public static Logger pobierzInstancje() {
// Pierwsze sprawdzenie — bez synchronizacji (szybka ścieżka)
if (instancja == null) {
// Synchronizacja tylko przy tworzeniu instancji — nie przy każdym dostępie
synchronized (Logger.class) {
// Drugie sprawdzenie — już wewnątrz bloku synchronized
// Zabezpiecza przed sytuacją, gdy dwa wątki przeszły pierwsze sprawdzenie
if (instancja == null) {
instancja = new Logger();
}
}
}
return instancja;
}
public synchronized void log(String wiadomosc) {
// synchronized na metodzie — zapis do listy jest bezpieczny wątkowo
logi.add(wiadomosc);
}
}
Wersja nowoczesna — Enum Singleton (rekomendowana w Javie)
Joshua Bloch w Effective Java rekomenduje implementację Singletona przez enum — eliminuje ona problemy z serializacją i refleksją:
// Enum gwarantuje dokładnie jedną instancję na poziomie JVM
// Serializacja jest obsługiwana automatycznie
// Refleksja nie może złamać prywatności konstruktora
enum Logger {
INSTANCJA; // jedyna instancja — tworzona przez JVM przy pierwszym użyciu
private final List<String> logi = new ArrayList<>();
public synchronized void log(String wiadomosc) {
logi.add(wiadomosc);
System.out.println("[LOG] " + wiadomosc);
}
public List<String> pobierzLogi() {
return Collections.unmodifiableList(logi);
}
}
// Użycie jest równie proste:
Logger.INSTANCJA.log("Zamówienie złożone");
5. Diagram — jak działa Singleton?
┌─────────────────────────────────────────────────────────────────┐ │ APLIKACJA │ │ │ │ OrderService PaymentService ShippingService │ │ │ │ │ │ │ │ pobierzInstancje() │ pobierzInstancje() │ │ │ └──────────────────────┴──────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ Logger │ ← jedyna instancja │ │ │ ────────────── │ w pamięci JVM │ │ │ instancja: ●────┼──► [ten obiekt] │ │ │ logi: [...] │ │ │ └──────────────────┘ │ │ │ │ Każde wywołanie pobierzInstancje() zwraca TEN SAM obiekt │ └─────────────────────────────────────────────────────────────────┘ Przepływ tworzenia instancji: Pierwsze wywołanie: Kolejne wywołania: ────────────────── ────────────────── pobierzInstancje() pobierzInstancje() │ │ ▼ ▼ instancja == null? instancja == null? │ TAK │ NIE ▼ ▼ new Logger() zwróć istniejącą │ instancję ▼ (bez tworzenia) zapisz w polu statycznym │ ▼ zwróć instancję
6. Dlaczego Singleton jest ważny?
Kontrola nad zasobami — połączenia z bazą danych, gniazda sieciowe, uchwyty do plików konfiguracyjnych to zasoby, których liczba jest ograniczona. Singleton gwarantuje, że tworzymy je dokładnie raz.
Spójność stanu — gdy wiele komponentów aplikacji operuje na tym samym obiekcie, mamy pewność, że wszystkie widzą ten sam stan. Nie ma niespójności wynikającej z istnienia wielu kopii.
Globalny punkt dostępu — nie trzeba przekazywać referencji przez konstruktory kolejnych klas. Każdy komponent może pobrać instancję bezpośrednio.
Wpływ na pracę zespołu — wyraźna konwencja: klasa z metodą pobierzInstancje() jest sygnałem dla całego zespołu,
że chodzi o zasób współdzielony. To ogranicza przypadkowe tworzenie duplikatów.
Kiedy Singleton sprawia problemy?
Singleton ma swoich krytyków — i nie bez powodu. Warto znać ciemną stronę wzorca:
- Ukryta zależność — klasy używające Singletona nie deklarują tej zależności w konstruktorze. To utrudnia zrozumienie, od czego klasa naprawdę zależy.
- Trudność w testowaniu — Singleton globalny stan sprawia, że testy mogą na siebie wpływać. Jeden test modyfikuje Singletona, kolejny test dostaje zmieniony stan.
- Naruszenie SRP — klasa Singleton zarządza własnym cyklem życia i wykonuje swoją główną odpowiedzialność.
- Problemy ze skalowaniem — w architekturach wieloserwerowych (mikroserwisy) Singleton gwarantuje jedyność tylko w ramach jednej instancji JVM.
W nowoczesnych aplikacjach Spring i inne kontenery IoC przejęły odpowiedzialność za zarządzanie cyklem życia obiektów.
@Componentw Springu jest domyślnie Singletonem — tworzonym i zarządzanym przez kontener, bez wad klasycznej implementacji.
7. Kiedy stosować?
Singleton ma sens, gdy:
- obiekt zarządza współdzielonym zasobem (pula połączeń, konfiguracja, rejestr zdarzeń),
- tworzenie obiektu jest kosztowne i nie ma sensu robić tego wielokrotnie,
- potrzebujemy jednego punktu koordynacji w aplikacji (np. menedżer wątków, cache),
- pracujemy z zewnętrznym API wymagającym jednej sesji (sterownik drukarki, połączenie z urządzeniem).
Singleton w popularnych frameworkach:
- Spring Framework — każdy
@Beanjest domyślnie Singletonem w kontekście aplikacji (ApplicationContext), - Hibernate —
SessionFactoryjest klasycznym Singletonem: kosztowna inicjalizacja, wielokrotne użycie, - Java Runtime —
Runtime.getRuntime()zwraca Singleton reprezentujący środowisko JVM.
Sygnały, że Singleton to dobry wybór:
- musisz zapewnić jeden punkt zapisu logów dla całej aplikacji,
- tworzysz pulę połączeń z bazą danych,
- ładujesz konfigurację z pliku — wczytaj raz, używaj wszędzie,
- implementujesz cache — jeden obiekt przechowuje wspólny bufor danych.
Sygnały ostrzegawcze — rozważ alternatywę (Dependency Injection):
- instancja Singletona przechowuje stan zmieniany przez wiele komponentów,
- klasy używające Singletona są trudne do testowania jednostkowego,
- potrzebujesz różnych konfiguracji Singletona w różnych środowiskach (dev/prod).
Trzy filary wzorca do zapamiętania:
1. PRYWATNY konstruktor → nikt nie może zrobić new Logger() 2. STATYCZNE pole → instancja żyje na poziomie klasy, nie obiektu 3. STATYCZNA metoda → jedyna brama wejściowa do instancji