Wzorce projektowe – Singleton

Wzorzec Singleton należy do grupy wzorców kreacyjnych. Jego celem jest ograniczenie tworzenia obiektów danej klasy do jednej instancji, dzięki czemu w całej aplikacji mamy dostęp do tego samego obiektu.

Najprostsza implementacja wygląda tak:

public final class SingletonClassNaiveImplementation {
    private static SingletonClassNaiveImplementation INSTANCE;

    private SingletonClassNaiveImplementation() {}

    public static SingletonClassNaiveImplementation getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SingletonClassNaiveImplementation();
        }
        return INSTANCE;
    }
}

Taka forma jest nazywana implementacją naiwną, ponieważ prawidłowo przedstawia założenia wzorca singleton, ale w praktyce występują problemy przy wielowątkowości. Na stronie https://github.com/mirekgab/singletonPattern zamieściłem krótki program, który pokazuje problemy z taką implementacją.

Poniżej przedstawiam poprawioną implementację tego wzorca, która zachowuje się prawidłowo przy działaniu na wielu wątkach.

public final class SingletonClass {
    private static volatile SingletonClass INSTANCE;

    private SingletonClass() {}

    public static SingletonClass getInstance() {
        SingletonClass result = INSTANCE;
        if (result != null) {
            return result;
        }

        synchronized (SingletonClass.class) {
            if (INSTANCE == null) {
                INSTANCE = new SingletonClass();
            }
            return INSTANCE;
        }
    }
}

W tej implementacji zastosowano dwa rozwiązania, które poprawiają zachowanie się obiektu w środowisku wielowątkowym:

  1. Deklaracja pola instance
private static volatile SingletonClass instance

W dużym skrócie słowo volatile sprawia, że JVM nie pozwoli wątkom na obsługę tej zmiennej z wykorzystaniem pamięci podręcznej, dzięki czemu wartość zapisana przez jeden wątek jest bezpośrednio dostępna dla innych wątków.

2. Blokada z podwójnym zatwierdzeniem – najpierw sprawdzam, czy zmienna INSTANCE ma wartość null, jeżeli nie to zwracam wartość. Natomiast jeżeli ta zmienna ma wartość null, to dopiero wtedy wykonuję blok oznaczony jako synchronized, w którym ponownie sprawdzam czy mam zwrócić istniejącą wartość, czy utworzyć nowy obiekt. Dzięki temu unikamy zakładania blokady na etapie sprawdzania czy singleton ma wartość null. Blokadę zakładamy dopiero wtedy, kiedy singleton ma wartość null i musimy utworzyć nowy obiekt.

Testy

Na stronie https://github.com/mirekgab/singletonPattern znajduje się program sprawdzający obie powyższe implementacje. Działanie programu polega na tym, że w każdym kroku pętli tworzę dwa obiekty w dwóch wątkach, a następnie obiekty te pobierają obiekt singleton i wypisują wartość, jaka została ustawiona w momencie wywołania metody getInstance obiektu singleton.

Poniżej wyniki tego testu.

singleton test naive implementation
 test count:  2    thread number: 2     value:naive singleton thread 2
 test count:  9    thread number: 2     value:naive singleton thread 2
 test count:  9    thread number: 1     value:naive singleton thread 1
 test count:  8    thread number: 2     value:naive singleton thread 2
 test count:  8    thread number: 1     value:naive singleton thread 1
 test count:  7    thread number: 2     value:naive singleton thread 2
 test count:  7    thread number: 1     value:naive singleton thread 1
 test count:  6    thread number: 2     value:naive singleton thread 2
 test count:  6    thread number: 1     value:naive singleton thread 1
 test count:  5    thread number: 2     value:naive singleton thread 2
 test count:  5    thread number: 1     value:naive singleton thread 1
 test count:  3    thread number: 1     value:naive singleton thread 1
 test count:  1    thread number: 2     value:naive singleton thread 2
 test count:  2    thread number: 1     value:naive singleton thread 1
 test count:  0    thread number: 1     value:naive singleton thread 1
 test count:  1    thread number: 1     value:naive singleton thread 1
 test count:  4    thread number: 2     value:naive singleton thread 2
 test count:  0    thread number: 2     value:naive singleton thread 2
 test count:  4    thread number: 1     value:naive singleton thread 1
 test count:  3    thread number: 2     value:naive singleton thread 2
 
 singleton test
 test count:  0    thread number: 2     value:singleton thread 2
 test count:  4    thread number: 2     value:singleton thread 2
 test count:  7    thread number: 2     value:singleton thread 2
 test count:  8    thread number: 1     value:singleton thread 2
 test count:  9    thread number: 1     value:singleton thread 2
 test count:  8    thread number: 2     value:singleton thread 2
 test count:  9    thread number: 2     value:singleton thread 2
 test count:  2    thread number: 2     value:singleton thread 2
 test count:  1    thread number: 1     value:singleton thread 2
 test count:  1    thread number: 2     value:singleton thread 2
 test count:  2    thread number: 1     value:singleton thread 2
 test count:  0    thread number: 1     value:singleton thread 2
 test count:  3    thread number: 1     value:singleton thread 2
 test count:  3    thread number: 2     value:singleton thread 2
 test count:  7    thread number: 1     value:singleton thread 2
 test count:  6    thread number: 2     value:singleton thread 2
 test count:  6    thread number: 1     value:singleton thread 2
 test count:  5    thread number: 2     value:singleton thread 2
 test count:  4    thread number: 1     value:singleton thread 2
 test count:  5    thread number: 1     value:singleton thread 2

Jak widać na powyższym wyniku, w implementacji naiwnej otrzymujemy dwa różne obiekty, natomiast w implementacji poprawionej, dwa obiekty z dwóch różnych wątków otrzymują ten sam singleton.