Wie verwende ich Singletons (Entwurfsmuster)?

In diesem Beitrag lernen Sie, was ein Singleton (Entwurfsmuster) ist, wann es eingesetzt werden kann und wie es aufgebaut ist.

Autor/in Mirko Eberlein
Datum 16.11.2017
Lesezeit 8 Minuten

Heute will ich euch einen Einblick in das Singleton-Entwurfsmuster geben. Bevor wir starten, schauen wir uns die Notwendigkeit eines solchen Musters an.

Stellen Sie sich vor, Sie besitzen ein Kaufhaus. Leider haben Sie aber keine Registerkassen. Nun geben Sie die Anweisung, dass alle Einkäufe auf einer Liste festgehalten werden. Sie haben 20 Mitarbeiter und diese stellen sich nicht immer schlau an. Es kommt oft vor, dass an einem Tag zwei oder sogar drei Listen angefangen werden. Zu allem Überfluss kommt es noch dazu, dass bestimmte Einkäufe auf mehr als einer der Listen festgehalten werden. Das bringt immer mehr Probleme mit sich.

Die Buchhaltung flucht, wenn sie die Listen am Abend zusammenführen und doppelte Einträge entfernen muss. Was machen Sie also als Kaufhaus-Chef? Richtig!! Sie sorgen dafür, dass es nur eine einzige Liste gibt und dass alle Einträge nur einmal erfasst werden. Sie brauchen die Einträge nicht doppelt, Sie benötigen nicht mehrere Listen, sondern nur eine.

Genauso ist es in der Programmierung. Es gibt Klassen, die aus Perfomance- oder Abgleichgründen nur einmal existieren sollten.

Wie implementiert man nun ein Singleton?

Die einfachste Form sieht wie folgt aus:

public class StandardSingleton {
       private static StandardSingleton instance = null;
       private static final String TEXT = "So einfach ist das!";
       private StandardSingleton(){}
       public static StandardSingleton getInstance(){
             if(instance == null){
                    instance = new StandardSingleton();
                    //hier können noch einmalige Aktionen beim Erstellen ausgeführt werden.
             }
             return instance;
       }
       public static String exampleFunktion(){
             return TEXT;
       }
}

Schauen wir uns den Code etwas genauer an:

Wir haben ein private Object der eigenen Klasse, das static ist und mit null initialisiert wird. Der final ist ein String, den wir zurückgeben wollen. Der Konstruktor ist private gesetzt, sodass er nur aus der Klasse selbst aufgerufen werden kann.

In der getInstance-Funktion prüfen wir, ob die Variable instance noch null ist. Falls dem so ist, wird eine neue Instanz erstellt. Die example-Funktion gibt uns einen String zurück. Nun wollen wir dies testen.

Das heisst, wir müssten einmal den richtigen String zurückbekommen und gleichzeitig sicherstellen, dass wir dieselbe Instanz bekommen. Dafür habe ich einen kleinen JUnit-Test geschrieben:

@Test
       public void testStandardSingleton(){
             String test = StandardSingleton.exampleFunktion();
             //ist der String korrekt Aufruftest
             assertTrue(test.equals("So einfach ist das!"));

             //auf gleichheit Testen 2 mal die selbe Instance ist Muss
             StandardSingleton s1 = StandardSingleton.getInstance();

             StandardSingleton s2 = StandardSingleton.getInstance();
             assertTrue(s1 == s2);
}

Anschliessend hole ich mir zwei StandardSingleton-Instanzen s1 und 2s. Mut derFunktion prüfe ich nun, ob die Instanzen gleich sind. Mit ==gehe ich sicher, dass es sich zu 100% um die gleiche Instanz handelt. Der Test ist erfolgreich, aber ist unser Singleton nun perfekt?

Das Thread-Problem

Nehmen wir nun dieses Singleton, das genauso aufgebaut ist wie im ersten Beispiel:

public class TimeProblemSingleton {
       private static TimeProblemSingleton instance = null;
       private TimeProblemSingleton(){}
       private static boolean first = true;
       public static TimeProblemSingleton getInstance(){
             if(instance == null){
                    timeDelayFunction();
                    instance = new TimeProblemSingleton();
             }
             return instance;
       }
       private static void timeDelayFunction() {
            try {
               if(first) {
                  first = false;
                  Thread.currentThread();
                           //Verzögerung von 50 Millisekunden
                    Thread.sleep(50);

              }
            }
            catch(InterruptedException ex) {
               System.out.println("Thread beendet");
            }
         }
}

Sie sehen, dass ich eine timeDelay-Function eingefügt habe. Ausserdem rufe ich diese im getInstance auf, falls das Object noch null ist. Nun vergeht in der Funktion Zeit, die ich hier mit der sleep-Funktion simuliere. Führen wir nun den Test noch einmal durch, lassen die String-Überprüfung weg und testen nur die Instanz.

Schauen wir uns dazu den Test an:

private static TimeProblemSingleton timeSingleton = null;
  @Test
    public void testTimeDelaySingleton() throws InterruptedException {
      // Nun erstellen wir Singletons in 2 Threads
      Thread t1 = new Thread(new SingletonThreadTest());

      Thread t2 = new Thread(new SingletonThreadTest());
      t1.start();
      t2.start();
      t1.join();
      t2.join();
    }
private static class SingletonThreadTest implements Runnable {
  public void run() {
    TimeProblemSingleton singleton = TimeProblemSingleton.getInstance();
    synchronized (TimeProblemSingleton.class) {
    if (singleton == null)
      timeSingleton = singleton;
    }
    assertTrue(singleton == timeSingleton);
  }
}

Dieser Test wird fehlschlagen. Das Problem ist einfach; ich rufe den ersten Thread auf und bekomme vor meinen synchronized den Delay. Dieser sorgt dafür, dass der nächste Thread an dieselbe Stelle gelangt und eine neue Instanz erstellt wird. Singleton ist somit != timeSingleton.

Wie läuft das ab?

Variable 1 ruft die getInstance-Funktion auf, startet in den Timedelay und wird dort neu erstellt. Da first noch false ist, wird der sleep aufgerufen und Thread 1 wartet 50 Millisekunden.

Thread 2 kommt an die Stelle, da aber first nun true ist, überspringt er den sleep und gibt eine neue Instanz zurück. In unserem Test wird das timeSingleton mit dieser Variable gesetzt. Thread 1 wacht auf und erstellt ebenfalls eine neue Instanz. Nur wird diese nun nicht gesetzt, da ja Thread 2 schon das timeSingleton initialisiert hat. Somit ist Instanz1 und Instanz2 unterschiedlich. Man kann also sagen, dass unsere Singleton so nicht Threadsave ist.

Doch wie kann ich das umgehen? Ganz einfach, ich ändere nur die getInstance-Funktion auf synchronized.

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

Lassen wir nun den Test laufen, werden wir sehen, dass es keinen Fehler mehr gibt.

Es gibt noch zwei weitere Möglichkeiten, einen Thread Save Singleton zu entwerfen. Einmal ziemlich einfach:

public class SimpleSingleton {
  public static final SimpleSingleton INSTANCE = new SimpleSingleton();
  private static final String FINALSTRING = "TEST";
  private SimpleSingleton(){}
  public String getTestString(){
    return FINALSTRING;
  }
}

Wir sehen hier eine einfache Umsetzung. Der Aufruf und der Test sind dann wie folgt aufgebaut:

@Test
public void testSimpleSingleton() throws InterruptedException {
  String test = SimpleSingleton.INSTANCE.getTestString();
  assertTrue(test.equals("TEST"));
}

Der Aufruf ist ziemlich einfach. Schränkt Sie aber in der Erstellung ein. Wenn Sie sich für diese einfache Art entscheiden, können Sie beim Erstellen allerdings keine anderen Funktionen ausführen.

Kommen wir zur letzten Implementation.

Enum als Singleton

Ich persönlich bevorzuge diese Variante. Ich verwende ein Enum als Singleton.

Nun erst einmal zum Code:

public enum EnumSingleton {
       INSTANCE;
       private final String test = "Test";
       private EnumSingleton(){
             //Konstruktor
       }

       public String exampleFunction(){
             return test;
       }
}

Der Code sieht relativ einfach aus, und das ist er auch. Ich erstelle ein Enum, nenne den einzigen Parameter INSTANCE. Nun kann ich alle Funktionen in diesem Enum über die Instance aufrufen. Ich bekomme Serialisation automatisch mitgeliefert. Das Singleton wird erstellt, sobald es das erste Mal aufgerufen wird.

Ein Test sieht wie folgt aus:

@Test
public void testEnumSingleton(){
       String test = EnumSingleton.INSTANCE.exampleFunction();
       assertTrue(test.equals("Test"));
}

Ganz egal, wie Sie sich entscheiden, denken Sie immer daran, wann es sinnvoll ist, ein Singleton einzusetzen.

Nutzen Sie es für Sachen, die Sie nur einmal in der Applikation benötigen. Achten Sie auf Engpässe. Wenn Sie komplexe Sachen in einem Singleton abwickeln und mit Multithreading arbeiten, haben Sie eventuell ein Zeitproblem. Thread 1 wartet, bis Thread2 das singleton freigibt.

Typische Anwendungsgebiete für Singletons

Das sind: Logging, Treiber Objekte, Caching und Thread Pooling.

Die Beispiele können unter github.com/bulli1979/SingletonExamples/tree/master angeschaut und verwendet werden.


Autor/in

Mirko Eberlein

Mirko Eberlein ist Senior Developer bei der Webgate Consulting AG und Trainer im Bereich Webentwicklung bei Digicomp. Er ist seit 17 Jahren im IT-Umfeld tätig und sich über die Jahre Wissen über verschiedene Programmiersprachen angeeignet. Er versucht sich stets neue Techniken und Trends anzusehen.