Java: Thread-safe Lazy Initialization

von Hubert Schmid vom 2014-06-08

In Java gibt es drei verbreite Verfahren zur Umsetzung der Lazy Initialization. Erstens durch vollständige Serialisierung mit synchronized, zweitens mit Hilfe des sogenannten Double-checked Locking, und drittens durch das Initialization-on-demand Holder Idiom. Zur Abgrenzung lässt sich feststellen, dass die letzten Beiden ungefähr zwei Größenordnungen schneller als die Erste sind, und dass die ersten Beiden allgemein nutzbar sind, wohingegen die Letzte sich nur für Singletons eignet.

Die wirklich interessanten Frage sind jedoch: Warum überhaupt so kompliziert? Warum bietet Java nicht eine einfache Lösung für die Lazy Initialization an, so wie es andere Programmiersprachen auch tun? Warum müssen sich Java-Entwickler für eine anscheinend häufig gegebene Anforderung mit diesen technischen Details auseinandersetzen?

Auf diese Fragen habe ich keine Antworten. Aber ich habe eine Vorstellung dazu, wie eine entsprechende Unterstützung aussehen könnte, und genau darüber schreibe ich im Folgenden.

Deferred

Bei der Lazy Initialization lassen sich grob zwei Fälle unterscheiden, die ich im folgenden als Deferred und Once bezeichne. Zunächst zur Ersteren: In diesem Fall ist bereits initial bekannt, wie das Objekt erzeugt wird. Dennoch soll die Erzeugung – aus bestimmten Gründen – erst so spät wie möglich erfolgen.

Bevor ich auf die Definition der Klasse Deferred eingehe, widme ich mich ihrer Verwendung. Das folgende Listing zeigt die Klasse DriverManager, von der es nach dem Singleton-Muster nur eine Instanz geben soll. Gelöst wird das über eine statische Member-Variable des Typs Deferred<DriverManager>, die initial den Bauplan für die Instanz übergeben bekommt, ihn aber erst bei der ersten Verwendung ausführt. Der private Konstruktor von DriverManager verhindert zudem, dass noch an anderen Stellen Instanzen erzeugt werden können.

public final class DriverManager { private static final Deferred<DriverManager> instance = new Deferred<>(DriverManager::new); public static DriverManager getInstance() { return instance.get(); } private DriverManager() { // ... } }

Eine mögliche und effiziente Implementierung der Klasse Deferred ist im folgenden Listing zu sehen. Die Implementierung verwendet intern das Double-checked Locking, das sich für solche Fälle besonders gut eignet. Außerdem ist sichergestellt, dass eine geworfene Ausnahme bei der Erzeugung nicht zu einer fehlerhaften Initialisierung führt. Mehr gibt es zu dieser Implementierung eigentlich nicht zu sagen.

public final class Deferred<T> { private volatile Supplier<T> supplier = null; private T object = null; public Deferred(Supplier<T> supplier) { this.supplier = Objects.requireNonNull(supplier); } public T get() { if (supplier != null) { synchronized (this) { if (supplier != null) { object = supplier.get(); supplier = null; } } } return object; } }

Once

In manchen Fällen steht erst mit dem ersten Zugriff fest, wie das Objekt konstruiert werden soll – beispielsweise weil eine Ressource von außerhalb für die Konstruktion benötigt wird. Das ist ein Fall für Once, wobei wiederum sichergestellt wird, dass die Erzeugung nur einmal erfolgt.

Zunächst ein Beispiel für die Verwendung: Dieses Mal wird die Klasse DriverManager mit einer Resource erzeugt, die beim ersten Aufruf der Methode getInstance übergeben wird. Gelöst wird das über eine statische Member-Variable vom Typ Once<DriverManager>, die initial keine weiteren Informationen hat. Beim Aufruf der get-Methode wird festgestellt, ob die Initialisierung bereits abgeschlossen wurde, und andernfalls ein Objekt mit den nun vorhandenen Informationen zu erzeugen.

public final class DriverManager { private static final Once<DriverManager> instance = new Once<>(); public static DriverManager getInstance(Resource resource) { return instance.get(() -> new DriverManager(resource)); } private DriverManager(Resource resource) { // ... } }

Eine mögliche und effiziente Implementierung der Klasse Once ist im folgenden Listing zu sehen. Wiederum verwendet die Implementierung intern das Double-checked Locking, das auch für diesen Fall bestens geeignet ist. Auch zu dieser Implementierung ist ansonsten eigentlich nichts mehr hinzuzufügen.

public final class Once<T> { private volatile boolean initialized = false; private T object = null; public T get(Supplier<T> supplier) { if (!initialized) { synchronized (this) { if (!initialized) { object = supplier.get(); initialized = true; } } } return object; } }

Fazit

Die beiden Beispiele für Deferred und Once zeigen vor allem, wie einfach es für Java 8 wäre, die Lazy Initialization durch die Standardbibliothek zu unterstützen. Das würde vielen Softwareentwicklern helfen, und vermutlich auch zahlreiche falsche oder übermäßig komplizierte Alternativen verhindern. Doch bedauerlicherweise wurde das mit der Einführung von Java 8 versäumt, und so werden wir auch die nächsten Jahre Diskussionen über die beste Art der Lazy Initialization führen.