Java-Tipp 67: Faule Instanziierung

Es ist noch nicht lange her, dass wir von der Aussicht begeistert waren, den integrierten Speicher in einem 8-Bit-Mikrocomputersprung von 8 KB auf 64 KB zu haben. Angesichts der ständig wachsenden, ressourcenintensiven Anwendungen, die wir jetzt verwenden, ist es erstaunlich, dass es jemals jemandem gelungen ist, ein Programm zu schreiben, das in diese winzige Menge an Speicher passt. Während wir heutzutage viel mehr Gedächtnis haben, können wir aus den Techniken, die entwickelt wurden, um unter solch engen Bedingungen zu arbeiten, einige wertvolle Lehren ziehen.

Darüber hinaus geht es bei der Java-Programmierung nicht nur darum, Applets und Anwendungen für die Bereitstellung auf PCs und Workstations zu schreiben. Java hat auch auf dem Markt für eingebettete Systeme starke Fortschritte erzielt. Gegenwärtige eingebettete Systeme verfügen über relativ knappe Speicherressourcen und Rechenleistung, so dass viele der alten Probleme, mit denen Programmierer konfrontiert sind, für Java-Entwickler, die im Gerätebereich arbeiten, wieder aufgetaucht sind.

Das Abwägen dieser Faktoren ist ein faszinierendes Designproblem: Es ist wichtig zu akzeptieren, dass keine Lösung im Bereich des eingebetteten Designs perfekt ist. Wir müssen also die Arten von Techniken verstehen, die nützlich sein werden, um das feine Gleichgewicht zu erreichen, das erforderlich ist, um innerhalb der Einschränkungen der Bereitstellungsplattform zu arbeiten.

Eine der Speichererhaltungstechniken, die Java-Programmierer nützlich finden, ist die verzögerte Instanziierung. Bei der verzögerten Instanziierung erstellt ein Programm erst dann bestimmte Ressourcen, wenn die Ressource zum ersten Mal benötigt wird. Dadurch wird wertvoller Speicherplatz frei. In diesem Tipp untersuchen wir verzögerte Instanziierungstechniken beim Laden von Java-Klassen und beim Erstellen von Objekten sowie die besonderen Überlegungen, die für Singleton-Muster erforderlich sind. Das Material in diesem Tipp stammt aus der Arbeit in Kapitel 9 unseres Buches Java in der Praxis: Entwurfsstile und Redewendungen für effektives Java (siehe Ressourcen).

Eifrige vs. faule Instanziierung: ein Beispiel

Wenn Sie mit dem Webbrowser von Netscape vertraut sind und beide Versionen 3.x und 4.x verwendet haben, haben Sie zweifellos einen Unterschied beim Laden der Java-Laufzeit festgestellt. Wenn Sie sich beim Start von Netscape 3 den Begrüßungsbildschirm ansehen, werden Sie feststellen, dass verschiedene Ressourcen geladen werden, einschließlich Java. Wenn Sie Netscape 4.x starten, wird die Java-Laufzeit jedoch nicht geladen. Es wird gewartet, bis Sie eine Webseite besuchen, die das Tag enthält. Diese beiden Ansätze veranschaulichen die Techniken der eifrigen Instanziierung (Laden, falls erforderlich) und der verzögerten Instanziierung (Warten Sie, bis sie angefordert wird, bevor Sie sie laden, da sie möglicherweise nie benötigt wird).

Beide Ansätze weisen Nachteile auf: Einerseits verschwendet das Laden einer Ressource möglicherweise wertvollen Speicher, wenn die Ressource während dieser Sitzung nicht verwendet wird. Wenn es jedoch nicht geladen wurde, zahlen Sie den Preis in Bezug auf die Ladezeit, wenn die Ressource zum ersten Mal benötigt wird.

Betrachten Sie die verzögerte Instanziierung als eine Richtlinie zur Ressourcenschonung

Die verzögerte Instanziierung in Java fällt in zwei Kategorien:

  • Faules Laden der Klasse
  • Lazy Objekterstellung

Faules Laden der Klasse

Die Java-Laufzeit verfügt über eine integrierte verzögerte Instanziierung für Klassen. Klassen werden nur dann in den Speicher geladen, wenn sie zum ersten Mal referenziert werden. (Sie können auch zuerst über HTTP von einem Webserver geladen werden.)

MyUtils.classMethod (); // erster Aufruf einer statischen Klassenmethode Vector v = new Vector (); // erster Aufruf an Operator neu

Das verzögerte Laden von Klassen ist ein wichtiges Merkmal der Java-Laufzeitumgebung, da es unter bestimmten Umständen die Speichernutzung reduzieren kann. Wenn beispielsweise ein Teil eines Programms während einer Sitzung niemals ausgeführt wird, werden Klassen, auf die nur in diesem Teil des Programms verwiesen wird, niemals geladen.

Lazy Objekterstellung

Die Erstellung fauler Objekte ist eng mit dem Laden fauler Klassen verbunden. Wenn Sie das neue Schlüsselwort zum ersten Mal für einen Klassentyp verwenden, der zuvor noch nicht geladen wurde, wird es von der Java-Laufzeit für Sie geladen. Durch die verzögerte Objekterstellung kann die Speichernutzung viel stärker reduziert werden als durch das verzögerte Laden von Klassen.

Um das Konzept der faulen Objekterstellung vorstellen, lassen Sie sich einen Blick auf einem einfachen Code - Beispiel , in dem ein Frameeine verwendeten MessageBoxFehler Display - Meldungen:

öffentliche Klasse MyFrame erweitert Frame {private MessageBox mb_ = new MessageBox (); // privater Helfer, der von dieser Klasse verwendet wird private void showMessage (String message) {// setze den Nachrichtentext mb_.setMessage (message); mb_.pack (); mb_.show (); }}

Im obigen Beispiel wird beim Erstellen einer Instanz von auch MyFramedie MessageBoxInstanz mb_ erstellt. Die gleichen Regeln gelten rekursiv. Daher werden alle Instanzvariablen, die im MessageBoxKonstruktor der Klasse initialisiert oder zugewiesen wurden , auch außerhalb des Heaps usw. zugewiesen. Wenn die Instanz von MyFramenicht zum Anzeigen einer Fehlermeldung innerhalb einer Sitzung verwendet wird, verschwenden wir unnötig Speicher.

In diesem ziemlich einfachen Beispiel werden wir nicht wirklich zu viel gewinnen. Wenn Sie jedoch eine komplexere Klasse betrachten, die viele andere Klassen verwendet, die wiederum mehr Objekte rekursiv verwenden und instanziieren, ist die potenzielle Speichernutzung offensichtlicher.

Betrachten Sie die verzögerte Instanziierung als Richtlinie zur Reduzierung des Ressourcenbedarfs

Die träge Herangehensweise an das obige Beispiel ist unten aufgeführt, wo die object mb_beim ersten Aufruf von instanziiert wird showMessage(). (Das heißt, erst wenn es tatsächlich vom Programm benötigt wird.)

öffentliche letzte Klasse MyFrame erweitert Frame {private MessageBox mb_; // null, implizit // privater Helfer, der von dieser Klasse verwendet wird private void showMessage (String message) {if (mb _ == null) // erster Aufruf dieser Methode mb_ = new MessageBox (); // setze den Nachrichtentext mb_.setMessage (message); mb_.pack (); mb_.show (); }}

If you take a closer look at showMessage(), you'll see that we first determine whether the instance variable mb_ is equal to null. As we haven't initialized mb_ at its point of declaration, the Java runtime has taken care of this for us. Thus, we can safely proceed by creating the MessageBox instance. All future calls to showMessage() will find that mb_ is not equal to null, therefore skipping the creation of the object and using the existing instance.

A real-world example

Let's now examine a more realistic example, where lazy instantiation can play a key role in reducing the amount of resources used by a program.

Angenommen, wir wurden von einem Client gebeten, ein System zu schreiben, mit dem Benutzer Bilder in einem Dateisystem katalogisieren und entweder Miniaturansichten oder vollständige Bilder anzeigen können. Unser erster Versuch könnte darin bestehen, eine Klasse zu schreiben, die das Bild in ihren Konstruktor lädt.

öffentliche Klasse ImageFile {private String filename_; privates Bild image_; public ImageFile (String filename) {filename_ = filename; // lade das Bild} public String getName () {return filename_;} public Image getImage () {return image_; }}

Im obigen Beispiel wird ImageFileein übermäßiger Ansatz zum Instanziieren des ImageObjekts implementiert . Zu seinen Gunsten garantiert dieses Design, dass ein Bild zum Zeitpunkt eines Anrufs bei sofort verfügbar ist getImage(). Dies kann jedoch nicht nur schmerzhaft langsam sein (im Fall eines Verzeichnisses mit vielen Bildern), sondern dieses Design kann auch den verfügbaren Speicher erschöpfen. Um diese potenziellen Probleme zu vermeiden, können wir die Leistungsvorteile des sofortigen Zugriffs gegen eine geringere Speichernutzung eintauschen. Wie Sie vielleicht erraten haben, können wir dies erreichen, indem wir die verzögerte Instanziierung verwenden.

Hier ist die aktualisierte ImageFileKlasse, die denselben Ansatz wie die Klasse MyFramemit ihrer MessageBoxInstanzvariablen verwendet:

public class ImageFile { private String filename_; private Image image_; //=null, implicit public ImageFile(String filename) { //only store the filename filename_=filename; } public String getName(){ return filename_;} public Image getImage() { if(image_==null) { //first call to getImage() //load the image... } return image_; } } 

In this version, the actual image is loaded only on the first call to getImage(). So to recap, the trade-off here is that to reduce the overall memory usage and startup times, we pay the price for loading the image the first time it is requested -- introducing a performance hit at that point in the program's execution. This is another idiom that reflects the Proxy pattern in a context that requires a constrained use of memory.

The policy of lazy instantiation illustrated above is fine for our examples, but later on you'll see how the design has to alter in the context of multiple threads.

Lazy instantiation for Singleton patterns in Java

Let's now take a look at the Singleton pattern. Here's the generic form in Java:

public class Singleton { private Singleton() {} static private Singleton instance_ = new Singleton(); static public Singleton instance() { return instance_; } //public methods } 

In the generic version, we declared and initialized the instance_ field as follows:

static final Singleton instance_ = new Singleton(); 

Readers familiar with the C++ implementation of Singleton written by the GoF (the Gang of Four who wrote the book Design Patterns: Elements of Reusable Object-Oriented Software -- Gamma, Helm, Johnson, and Vlissides) may be surprised that we didn't defer the initialization of the instance_ field until the call to the instance() method. Thus, using lazy instantiation:

public static Singleton instance() { if(instance_==null) //Lazy instantiation instance_= new Singleton(); return instance_; } 

The listing above is a direct port of the C++ Singleton example given by the GoF, and frequently is touted as the generic Java version too. If you already are familiar with this form and were surprised that we didn't list our generic Singleton like this, you'll be even more surprised to learn that it is totally unnecessary in Java! This is a common example of what can occur if you port code from one language to another without considering the respective runtime environments.

For the record, the GoF's C++ version of Singleton uses lazy instantiation because there is no guarantee of the order of static initialization of objects at runtime. (See Scott Meyer's Singleton for an alternative approach in C++ .) In Java, we don't have to worry about these issues.

The lazy approach to instantiating a Singleton is unnecessary in Java because of the way in which the Java runtime handles class loading and static instance variable initialization. Previously, we have described how and when classes get loaded. A class with only public static methods gets loaded by the Java runtime on the first call to one of these methods; which in the case of our Singleton is

Singleton s=Singleton.instance(); 

The first call to Singleton.instance() in a program forces the Java runtime to load the class Singleton. As the field instance_ is declared as static, the Java runtime will initialize it after successfully loading the class. Thus guarantees that the call to Singleton.instance() will return a fully initialized Singleton -- get the picture?

Lazy instantiation: dangerous in multithreaded applications

Using lazy instantiation for a concrete Singleton is not only unnecessary in Java, it's downright dangerous in the context of multithreaded applications. Consider the lazy version of the Singleton.instance() method, where two or more separate threads are attempting to obtain a reference to the object via instance(). If one thread is preempted after successfully executing the line if(instance_==null), but before it has completed the line instance_=new Singleton(), another thread can also enter this method with instance_ still ==null -- nasty!

The outcome of this scenario is the likelihood that one or more Singleton objects will be created. This is a major headache when your Singleton class is, say, connecting to a database or remote server. The simple solution to this problem would be to use the synchronized key word to protect the method from multiple threads entering it at the same time:

synchronized static public instance() {...} 

However, this approach is a bit heavy-handed for most multithreaded applications using a Singleton class extensively, thereby causing blocking on concurrent calls to instance(). By the way, invoking a synchronized method is always much slower than invoking a nonsynchronized one. So what we need is a strategy for synchronization that doesn't cause unnecessary blocking. Fortunately, such a strategy exists. It is known as the double-check idiom.

The double-check idiom

Use the double-check idiom to protect methods using lazy instantiation. Here's how to implement it in Java:

public static Singleton instance() { if(instance_==null) //don't want to block here { //two or more threads might be here!!! synchronized(Singleton.class) { //must check again as one of the //blocked threads can still enter if(instance_==null) instance_= new Singleton();//safe } } return instance_; } 

The double-check idiom improves performance by using synchronization only if multiple threads call instance() before the Singleton is constructed. Once the object has been instantiated, instance_ is no longer ==null, allowing the method to avoid blocking concurrent callers.

Die Verwendung mehrerer Threads in Java kann sehr komplex sein. Tatsächlich ist das Thema Parallelität so umfangreich, dass Doug Lea ein ganzes Buch darüber geschrieben hat: Concurrent Programming in Java. Wenn Sie mit der gleichzeitigen Programmierung noch nicht vertraut sind, empfehlen wir Ihnen, eine Kopie dieses Buches zu erwerben, bevor Sie komplexe Java-Systeme schreiben, die auf mehreren Threads basieren.