Simon Migaj

Porównanie 9 implementacji singletona

Jedyny w swoim rodzaju, unikalny, niepowtarzalny i ciut samotny – SINGLETON 

Co omówimy?

Różne implementacje wzorca Java-Singleton

Polecam czytać jeden po drugim. Każdy jest potencjalnie lepszą wersją poprzedniej.

Jeśli lubisz książki to polecam przeczytać:  Rusz Głową – Wzorce Projektowe

 WZORCE PROJEKTOWE

Jak mówi Martin Fowler „Wzorce to tylko półprodukty – trzeba je dokończyć, aby wprowadzić je do swojego kodu.”

Wzorce są tylko szkieletami sprytnych mechanizmów. Abstrakcyjne pojęcia ułatwiające manipulację nad obiektami. Naszym zadaniem jest adaptacja tych konceptów do aplikacji. Wzorce redukują złożoność poprzez wprowadzenie gotowych już abstrakcji. Ponadto przenoszą rozmowę między developerami na wyższy poziom abstrakcji. Mówiąc, że zastanawiasz się nad zastosowaniem wzorca X przekazujesz w rzeczywistości bardzo wiele informacji. A to właśnie przekazywanie abstrakcyjnych informacji pozwoliło nam Homo Sapiens wyróżnić się na tle innych zwierząt. Oczywiście jest to skuteczne o ile oboje znacie koncepcje wzorców. Wzorce pozwolą ci zredukować złożoność twojego systemu. Ułatwią nowym osobom wejście do projektu. Zmniejszą ilość błędów poprzez zastosowanie uporządkowanych rozwiązań. Dadzą ci pakiet rozwiązań do typowego problemu. A do tego ułatwi komunikację z innymi programistami.

Ehh… Brzmi zbyt pięknie, co nie? Czasami są przypadki kiedy lekko wepchniesz, dopasujesz wzorzec do swojego kodu. Jest to okej jeśli poprawiasz tym czytelność kodu. Niemniej stosuj to z umiarem. Nie dąż do uzyskania ‚idealnego’ wzorca, bo to może tylko zwiększyć złożoność systemu. Kolejną pułapką jakiej powinieneś się wystrzegać jest POKUSA wpychania na chama wzorców tam gdzie ich nie potrzeba. Wzorce to cenne narzędzie do walki ze złożonością, ale trzeba używać z rozwagą jak wszystkiego innego! : )

SINGLETON

Umożliwia utworzenie tylko jednej instancji, która ma globalny dostęp. W praktyce sprowadza się do tego, że nie można utworzyć obiektu bezpośrednio poprzez new Singleton. Robimy to poprzez wywołanie globalnej instancji obiektu Singleton.getInstance(). W większości przypadków powinno się unikać stosowania Singletona. Może on spowodować conajmniej mały ból głowy. A tego chcemy uniknąć, co nie? Jak wspomniałem w ogólnym wpisie o wzorcach powinno się ich używać z głową. Nie stosować ich, bo ‚fajnie’ tylko jeśli na prawdę są one potrzebne. Czyli projektuj swoje aplikacje ze świadomością, iż wzorce istnieją, ale nie próbuj upychać ich gdzie popadnie i naginać aplikacji, żeby tylko się wpasowały.

Ogólne Zasady Tworzenia Singletona

public class Singleton {

    // Prywatna statyczna zmienna do przechowywania stworzonej klasy
    private static instance = new Singleton();
    
    // Prywatny konstruktor, aby ograniczyć tworzenie instancji klasy    
    private Singleton() {} 

    // Publiczna metoda zwracająca instancję klasy. To jest właśnie ten globalny dostęp do obiektu
    public static EagerInitializedSingleton getInstance(){ return instance; }
}

// Otrzymujemy dzięki temu następujący dostęp do klasy
Singleton.getInstance();

// Nie można zrobić tego
singleton = new Singleton();

 

1. Eager Initialization – Don’t use

public class EagerInitializedSingleton {
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
    
    private EagerInitializedSingleton() {}
    public static EagerInitializedSingleton getInstance(){ return instance; }
}

Oczywiście nie wyśmiewając uczelni – ale jest to iście uczelniana implementacja : ) 

Plusy

  • prosty do implementacji

Minusy

  • brak możliwości obsługi wyjątków
  • obiekt jest tworzony zawsze podczas ładowania się klas do pamięci
  • instancja może nie być wykorzystywana co powoduje marnowanie się zasobów

 

 

2. Static-Block Initialization – Don’t use

public class StaticBlockInitSingleton {
    private static StaticBlockInitSingleton instance;

    privateStaticBlockInitSingleton() {}
    
    static { 
        try{ instance = newStaticBlockInitSingleton(); }
        catch(Exception e){ throw new RuntimeException("Creating singleton instance failed"); } 
    }

    public static StaticBlockInitSingleton getInstance() { return instance; } 
}

Plusy

  • prosty do implementacji
  • statyczny block daje możliwość obsługi wyjątków

Minusy

  • obiekt jest tworzony zawsze podczas ładowania się klas do pamięci
  • instancja może nie być wykorzystywana co powoduje marnowanie się zasobów

Blok Statyczny static { ... } – jest wywoływany zawsze podczas tworzenia się klasy

 

 

 3. Lazy Initialization (Non-Thread-Safe) – Don’t use

public class LazyInitSingleton {
    private static LazyInitSingleton instance; 
    
    private LazyInitSingleton() {}
    
    public static LazyInitSingleton getInstance() {

        if(instance == null)
            instance = new LazyInitSingleton(); 
        return instance;
    } 
}

Plusy

  • stworzenie instancji klasy jest odłożone w czasie, aż do momentu pierwszego wywołania metody
  • możliwość obsługi wyjątków

Minusy

  • w przypadku wielowątkowych systemów utworzona zostanie instancja dla każdego wątku, co kompletnie burzy nam koncepcję singletona
Czym są wątki softwarowe? Tutaj

 

 

4. Lazy Initialization (Thread-Safe) – Don’t use

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;
    private ThreadSafeSingleton() {}

    public static synchronized ThreadSafeSingleton getInstance() { 
        if(instance == null)
            instance = new ThreadSafeSingleton();
        return instance;
    }
}

Plusy

  • stworzenie instancji klasy jest odłożone w czasie, aż do momentu pierwszego wywołania metody
  • możliwość obsługi wyjątków
  • metoda jest synchronized, oznacza to, że może ona być wykonywana tylko przez jeden wątek w danym czasie

Minusy

  • metoda synchronized spowalnia wydajność aplikacja, gdyż wiele wątków nie może mieć jednoczesnego dostępu do metody

 

 

5. Lazy Initialization – (Thread-Safe) – Not bad but don’t use

Lazy Initialization with Double Checked Locking

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;
    private ThreadSafeSingleton() {}

    public static ThreadSafeSingleton getInstance() { 
        if(instance == null)
            synchronized(ThreadSafeSingleton.class);
            if(instance == null)
                instance = new ThreadSafeSingleton();
        return instance;
    }
}

Tutaj dowiadujemy się niepozornej, a jednak mega interesującej konstrukcji. Stay tunned!

Plusy

  • stworzenie instancji klasy jest odłożone w czasie, aż do momentu pierwszego wywołania metody
  • możliwość obsługi wyjątków
  • lepsza wydajność, gdyż nie blokujemy metody

Minusy

  • tylko za pierwszym razem dotyka to wydajności aplikacji

 

 

6. Bill Pugh Singleton – Most widely used approach

public class BillPughSingleton {
    private BillPughSingleton() {}
    
    private static class SingletonHelper {
        
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() { return SingletonHelper.INSTANCE; }
}

W momencie ładowania się klasy BillPughSingleton wewnętrzna klasa private static class SingletonHelper jest pomijana. Skutkuje to tym, że obiekt  nie jest tworzony. Jest to podobne do Lazy Initialization, a do tego nie potrzebuje synchronized przez co jest thread-safe i nie blokuje wątku.

Plusy

  • proste w implementacji
  • nie blokuje wątków

 

 

7. Use Reflection to destroy all above – Cheating

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        // Tworzymy pierwszą instancję
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();

        // Tworzymy drugą instancję przy pomocy refleksji
        EagerInitializedSingleton instanceTwo = null;

        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {

                //TUTAJ - ustawiamy dostępność konstruktora
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break; 
            }
        } catch (Exception e) { e.printStackTrace(); }
    /**
    * Jak widać hashCode zwraca inny wynik w obu przypadkach. 
    * Oznacza to, że tworzenie Singletona się nie powiodło
    */
    System.out.println(instanceOne.hashCode()); // output: 821270921
    System.out.println(instanceTwo.hashCode()); // output: 116046083
    } 
}

Refleksja w Javie to bardziej zaawansowany temat. Tutaj przykłady użycia. A do tego fajnie jest to przedstawione w książce Java 8 – Horstmanna. Niemniej sam przykład jak i refleksja nie powinna być wykorzystywana na codzień. 🙂 

Jak to działa: 

Różne wartości HashCode() oznaczają, że tworzenie Singletona nie powiodło się. Refleksja sama w sobie jest stosunkowo rzadko używana. Jest to dość kontrowersyjny sposób na programowanie. Jak wszystko tak i to powinno być stosowane z umiarem. Według mnie dodaje ona niepotrzebnej złożoność do kodu. Z drugiej jednak strony mogą być przypadki gdzie będzie to jedyne rozwiązanie. Nie zbaczając z tematu wróćmy do wzorców : )

 

 

8. Enum Singleton – How to defend yourself from Yoda? 

public enum EnumSingleton {
    INSTANCE;
}

Z pomocą przychodzi Joshua Bloch – Efektywne Programowanie 

Plusy

  • proste w implementacji
  • zapewnia serializację, czyli nie potrzebujemy implementować interfejsu Serializable
  • refleksja nie jest już problemem

Minusy

Czy to jest najlepsza implementacja? 

Każdy używa według potrzeb. Nie ma jednoznacznej odpowiedzi. Pamiętajmy, że pola nie są serializowane, także jeśli serializujemy enum, a potem go deserializujemy pola, które przechowywały dane tracą je bezpowrotnie. Mimo wszystko jest to prawdopodobnie najlepsza metoda na tworzenie Singletona. A czym jest serializacja?

Halo, halo, ale jak tego używać?
public enum SingletonEnum {
    INSTANCE;
    int value;
    public int getValue() { return value; }
    public void setValue(int value) { this.value = value; }
}

public class EnumTest {
    public static void main(String[] args) {

        // Tworzymy instancję Singletona
        SingletonEnum singleton = SingletonEnum.INSTANCE;
        System.out.println(singleton.getValue()); // output: 0
        singleton.setValue(2);
        System.out.println(singleton.getValue()); // output: 2
    }
}

 

 

9. Serialization and Singleton – ugh… it’s not over yet? 

public class SerializedSingleton implements Serializable {
    private static final long serialVersionUID = -1104762320317737265L;

    private SerializedSingleton() {}
    
    private static class SingletonHelper { private static final SerializedSingleton instance = new SerializedSingleton(); }

    public static SerializedSingleton getInstance() { return SingletonHelper.instance; }

    // FIX FIX FIX
    protected Object readResolve() { return getInstance(); }
    // FIX FIX FIX
}
public class SingletonSerializedTest {
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton)
        in.readObject();
        in.close();

        System.out.println(instanceOne.hashCode()); // output: 213123123
        System.out.println(instanceTwo.hashCode()); // output: 678678678
    }
}

Problem z powyższą implementacją jest taki, że w momencie deserializacji jest tworzona nowa instancja obiektu. Rozwiązaniem tego problemu jest metoda, która jest wywoływana przed deserializacją protected Object readResolve() { return getInstance(); }. Wewnątrz tej metody wywołujemy getInstance() co zapewnia nam stworzenie tylko jednego obiektu.

 

DODATKI:

Czym jest Class-Loader?
  • JVM (Java-Virtual-Machine). Jest to po prostu aplikacja. Ma ona swój kod źródłowy. Można go debugować. Można go zmieniać i przebudowywać. Ładuje ona skompilowane pliki .class do pamięci.
  • JRE (Java-Runtime-Environment) zawiera w sobie JVM. To właśnie to jest odpowiedzialne za wykonywanie aplikacji Javowych.
  • Class-Loader istnieje on w JRE i jest odpowiedzialny za bezpośrednie ładowanie do pamięci Javowych klas.
 Czym jest Serializacja?

Jest to zwyczajnie zapisanie aktualnego stanu obiektu do formy bajtowej. Jest to platformowo niezależne, czyli można to przenieść do innego systemu i dopiero tam poddać to deserializacji. Po takim zabiegu obiekt ten jest wczytany ponownie do pamięci. Czym jest pamięć RAM: TUTAJ

 

Jeśli lubisz książki to najbardziej polecam przeczytać:  Rusz Głową – Wzorce Projektowe

ŹRÓDŁA KSIĄŻKOWE:

ŹRÓDŁA INTERNETOWE:

Zdjęcie główne autorstwa: Simon Migaj

23 Udostępnień