Suggerimento Java 67: istanziazione pigra

Non è passato molto tempo da quando eravamo entusiasti della prospettiva di avere la memoria integrata in un microcomputer a 8 bit che passava da 8 KB a 64 KB. A giudicare dalle applicazioni sempre crescenti e affamate di risorse che ora utilizziamo, è sorprendente che qualcuno sia mai riuscito a scrivere un programma che si adatti a quella piccola quantità di memoria. Sebbene abbiamo molta più memoria con cui giocare in questi giorni, alcune preziose lezioni possono essere apprese dalle tecniche stabilite per lavorare con vincoli così stretti.

Inoltre, la programmazione Java non consiste solo nella scrittura di applet e applicazioni per la distribuzione su personal computer e workstation; Java ha fatto grandi passi avanti anche nel mercato dei sistemi embedded. Gli attuali sistemi embedded hanno risorse di memoria e potenza di calcolo relativamente scarse, quindi molti dei vecchi problemi che devono affrontare i programmatori sono riemersi per gli sviluppatori Java che lavorano nel regno dei dispositivi.

Il bilanciamento di questi fattori è un affascinante problema di progettazione: è importante accettare il fatto che nessuna soluzione nell'area del design integrato sarà perfetta. Quindi, dobbiamo comprendere i tipi di tecniche che saranno utili per raggiungere il sottile equilibrio necessario per lavorare entro i vincoli della piattaforma di distribuzione.

Una delle tecniche di conservazione della memoria che i programmatori Java trovano utile è l' istanza pigra. Con l'istanza pigra, un programma si astiene dalla creazione di determinate risorse fino a quando la risorsa non è prima necessaria, liberando prezioso spazio di memoria. In questo suggerimento, esaminiamo le tecniche di istanziazione pigra nel caricamento di classi Java e nella creazione di oggetti e le considerazioni speciali richieste per i modelli Singleton. Il materiale in questo suggerimento deriva dal lavoro nel capitolo 9 del nostro libro, Java in Practice: Design Styles & Idioms for Effective Java (vedi Risorse).

Istanziazione desiderosa e pigra: un esempio

Se hai familiarità con il browser Web di Netscape e hai utilizzato entrambe le versioni 3.x e 4.x, senza dubbio avrai notato una differenza nel modo in cui viene caricato il runtime Java. Se guardi la schermata iniziale all'avvio di Netscape 3, noterai che carica varie risorse, incluso Java. Tuttavia, quando si avvia Netscape 4.x, non carica il runtime Java: attende fino a quando non si visita una pagina Web che include il tag. Questi due approcci illustrano le tecniche di istanziazione desiderosa (caricarla nel caso sia necessario) e istanziazione lazy (attendere che venga richiesta prima di caricarla, poiché potrebbe non essere mai necessaria).

Ci sono svantaggi in entrambi gli approcci: da un lato, caricare sempre una risorsa potenzialmente spreca memoria preziosa se la risorsa non viene utilizzata durante quella sessione; d'altra parte, se non è stata caricata, si paga il prezzo in termini di tempo di caricamento quando la risorsa viene richiesta per la prima volta.

Considera l'istanza pigra come una politica di conservazione delle risorse

L'istanza pigra in Java si divide in due categorie:

  • Lazy class loading
  • Creazione di oggetti pigri

Lazy class loading

Il runtime Java dispone di istanze pigre incorporate per le classi. Le classi vengono caricate in memoria solo al primo riferimento. (Possono anche essere caricati prima da un server Web tramite HTTP.)

MyUtils.classMethod (); // prima chiamata a un metodo di classe statica Vector v = new Vector (); // prima chiamata all'operatore new

Il caricamento lento delle classi è una caratteristica importante dell'ambiente runtime Java in quanto può ridurre l'utilizzo della memoria in determinate circostanze. Ad esempio, se una parte di un programma non viene mai eseguita durante una sessione, le classi a cui si fa riferimento solo in quella parte del programma non verranno mai caricate.

Creazione di oggetti pigri

La creazione di oggetti pigri è strettamente collegata al caricamento lento di classi. La prima volta che si utilizza la nuova parola chiave su un tipo di classe che in precedenza non è stato caricato, il runtime Java lo caricherà automaticamente. La creazione di oggetti pigri può ridurre l'utilizzo della memoria in misura molto maggiore rispetto al caricamento lento di classi.

Per introdurre il concetto di creazione di oggetti pigri, diamo un'occhiata a un semplice esempio di codice in cui a Frameutilizza a MessageBoxper visualizzare i messaggi di errore:

la classe pubblica MyFrame estende Frame {private MessageBox mb_ = new MessageBox (); // helper privato usato da questa classe private void showMessage (String message) {// imposta il testo del messaggio mb_.setMessage (message); mb_.pack (); mb_.show (); }}

Nell'esempio precedente, quando MyFrameviene creata un'istanza di, viene creata anche l' MessageBoxistanza mb_. Le stesse regole si applicano in modo ricorsivo. Quindi anche tutte le variabili di istanza inizializzate o assegnate nel MessageBoxcostruttore della classe vengono allocate fuori dall'heap e così via. Se l'istanza di MyFramenon viene utilizzata per visualizzare un messaggio di errore all'interno di una sessione, stiamo sprecando memoria inutilmente.

In questo esempio piuttosto semplice, non guadagneremo davvero molto. Ma se si considera una classe più complessa, che utilizza molte altre classi, che a loro volta utilizzano e istanziano più oggetti in modo ricorsivo, il potenziale utilizzo della memoria è più evidente.

Considera l'istanza pigra come una politica per ridurre i requisiti di risorse

L'approccio pigro all'esempio precedente è elencato di seguito, dove object mb_viene creata un'istanza alla prima chiamata a showMessage(). (Cioè, non finché non è effettivamente necessario al programma.)

classe finale pubblica MyFrame estende Frame {MessageBox privato mb_; // null, implicit // helper privato usato da questa classe private void showMessage (String message) {if (mb _ == null) // prima chiamata a questo metodo mb_ = new MessageBox (); // imposta il testo del messaggio mb_.setMessage (message); mb_.pack (); mb_.show (); }}

Se guardi più da vicino showMessage(), vedrai che per prima cosa determiniamo se la variabile di istanza mb_ è uguale a null. Poiché non abbiamo inizializzato mb_ al suo punto di dichiarazione, il runtime Java si è occupato di questo per noi. Quindi, possiamo procedere in sicurezza creando l' MessageBoxistanza. Tutte le future chiamate a showMessage()scopriranno che mb_ non è uguale a null, saltando quindi la creazione dell'oggetto e utilizzando l'istanza esistente.

Un esempio del mondo reale

Esaminiamo ora un esempio più realistico, in cui l'istanza pigra può svolgere un ruolo chiave nel ridurre la quantità di risorse utilizzate da un programma.

Supponiamo che un cliente ci abbia chiesto di scrivere un sistema che consenta agli utenti di catalogare le immagini su un filesystem e fornisca la possibilità di visualizzare le miniature o le immagini complete. Il nostro primo tentativo potrebbe essere quello di scrivere una classe che carichi l'immagine nel suo costruttore.

public class ImageFile {private String filename_; immagine immagine privata_; public ImageFile (String filename) {filename_ = filename; // carica l'immagine} public String getName () {return filename_;} public Image getImage () {return image_; }}

Nell'esempio sopra, ImageFileimplementa un approccio eccessivo per creare un'istanza Imagedell'oggetto. A suo favore, questo design garantisce che un'immagine sarà immediatamente disponibile al momento di una chiamata a getImage(). Tuttavia, non solo questo potrebbe essere dolorosamente lento (nel caso di una directory contenente molte immagini), ma questo design potrebbe esaurire la memoria disponibile. Per evitare questi potenziali problemi, possiamo scambiare i vantaggi in termini di prestazioni dell'accesso istantaneo con un utilizzo ridotto della memoria. Come avrai intuito, possiamo ottenere questo risultato utilizzando un'istanza pigra.

Ecco la ImageFileclasse aggiornata che utilizza lo stesso approccio della classe MyFramecon la sua MessageBoxvariabile di istanza:

public class ImageFile {private String filename_; immagine immagine privata_; // = null, file immagine pubblico implicito (String filename) {// memorizza solo il nome del file filename_ = filename; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// prima chiamata a getImage () // carica l'immagine ...} return image_; }}

In questa versione, l'immagine effettiva viene caricata solo alla prima chiamata a getImage(). Quindi, per ricapitolare, il compromesso qui è che per ridurre l'utilizzo complessivo della memoria ei tempi di avvio, paghiamo il prezzo per il caricamento dell'immagine la prima volta che viene richiesta, introducendo un calo delle prestazioni a quel punto nell'esecuzione del programma. Questo è un altro idioma che riflette il Proxymodello in un contesto che richiede un uso limitato della memoria.

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.

L'utilizzo di più thread in Java può essere molto complesso. In effetti, l'argomento della concorrenza è così vasto che Doug Lea ha scritto un intero libro su di esso: Concurrent Programming in Java. Se non conosci la programmazione concorrente, ti consigliamo di ottenere una copia di questo libro prima di iniziare a scrivere sistemi Java complessi che si basano su più thread.