Consejo 67 de Java: instanciación diferida

No fue hace tanto tiempo que estábamos encantados con la perspectiva de que la memoria integrada en un microordenador de 8 bits pasara de 8 KB a 64 KB. A juzgar por las aplicaciones cada vez más hambrientas de recursos que usamos ahora, es sorprendente que alguien haya logrado escribir un programa que se ajuste a esa pequeña cantidad de memoria. Si bien tenemos mucha más memoria para jugar en estos días, se pueden aprender algunas lecciones valiosas de las técnicas establecidas para trabajar con restricciones tan estrictas.

Además, la programación de Java no se trata solo de escribir applets y aplicaciones para su implementación en computadoras personales y estaciones de trabajo; Java también ha hecho grandes avances en el mercado de sistemas integrados. Los sistemas integrados actuales tienen recursos de memoria y potencia de cálculo relativamente escasos, por lo que muchos de los viejos problemas que enfrentan los programadores han resurgido para los desarrolladores de Java que trabajan en el ámbito de los dispositivos.

Equilibrar estos factores es un problema de diseño fascinante: es importante aceptar el hecho de que ninguna solución en el área del diseño integrado será perfecta. Por lo tanto, debemos comprender los tipos de técnicas que serán útiles para lograr el delicado equilibrio necesario para trabajar dentro de las limitaciones de la plataforma de implementación.

Una de las técnicas de conservación de memoria que los programadores de Java encuentran útil es la instanciación perezosa. Con la instanciación perezosa, un programa se abstiene de crear ciertos recursos hasta que el recurso se necesita primero, liberando un valioso espacio de memoria. En este consejo, examinamos las técnicas de instanciación diferida en la carga de clases de Java y la creación de objetos, y las consideraciones especiales necesarias para los patrones Singleton. El material de este consejo se deriva del trabajo del Capítulo 9 de nuestro libro, Java en la práctica: Estilos de diseño y modismos para un Java eficaz (consulte Recursos).

Creación de instancias ansiosa vs perezosa: un ejemplo

Si está familiarizado con el navegador web de Netscape y ha utilizado ambas versiones 3.xy 4.x, sin duda habrá notado una diferencia en cómo se carga el tiempo de ejecución de Java. Si observa la pantalla de presentación cuando se inicia Netscape 3, notará que carga varios recursos, incluido Java. Sin embargo, cuando inicia Netscape 4.x, no carga el tiempo de ejecución de Java; espera hasta que visite una página web que incluye la etiqueta. Estos dos enfoques ilustran las técnicas de instanciación ansiosa (cárguelo en caso de que sea necesario) y instanciación perezosa (espere hasta que se solicite antes de cargarlo, ya que es posible que nunca sea necesario).

Hay inconvenientes en ambos enfoques: por un lado, siempre cargar un recurso potencialmente desperdicia una memoria preciosa si el recurso no se usa durante esa sesión; por otro lado, si no se ha cargado, usted paga el precio en términos de tiempo de carga cuando se requiere el recurso por primera vez.

Considere la instanciación perezosa como una política de conservación de recursos

La instanciación perezosa en Java se divide en dos categorías:

  • Carga de clase perezosa
  • Creación de objetos perezosos

Carga de clase perezosa

El tiempo de ejecución de Java tiene una instanciación diferida incorporada para las clases. Las clases se cargan en la memoria solo cuando se hace referencia a ellas por primera vez. (También pueden cargarse primero desde un servidor web a través de HTTP).

MyUtils.classMethod (); // primera llamada a un método de clase estática Vector v = new Vector (); // primera llamada al operador nuevo

La carga de clases diferida es una característica importante del entorno de ejecución de Java, ya que puede reducir el uso de memoria en determinadas circunstancias. Por ejemplo, si una parte de un programa nunca se ejecuta durante una sesión, las clases referenciadas solo en esa parte del programa nunca se cargarán.

Creación de objetos perezosos

La creación de objetos diferidos está estrechamente relacionada con la carga de clases diferida. La primera vez que use la nueva palabra clave en un tipo de clase que no se haya cargado previamente, el tiempo de ejecución de Java lo cargará por usted. La creación diferida de objetos puede reducir el uso de la memoria en mayor medida que la carga diferida de clases.

Para presentar el concepto de creación de objetos perezosos, echemos un vistazo a un ejemplo de código simple donde a Frameusa a MessageBoxpara mostrar mensajes de error:

La clase pública MyFrame extiende Frame {Private MessageBox mb_ = new MessageBox (); // ayudante privado usado por esta clase private void showMessage (mensaje de cadena) {// establece el texto del mensaje mb_.setMessage (mensaje); mb_.pack (); mb_.show (); }}

En el ejemplo anterior, cuando MyFramese crea una instancia de, también se crea la MessageBoxinstancia mb_. Las mismas reglas se aplican de forma recursiva. Entonces, cualquier variable de instancia inicializada o asignada en MessageBoxel constructor de la clase también se asigna fuera del montón y así sucesivamente. Si la instancia de MyFrameno se usa para mostrar un mensaje de error dentro de una sesión, estamos desperdiciando memoria innecesariamente.

En este ejemplo bastante simple, realmente no vamos a ganar demasiado. Pero si considera una clase más compleja, que usa muchas otras clases, que a su vez usan e instancian más objetos de manera recursiva, el uso potencial de memoria es más evidente.

Considere la instanciación diferida como una política para reducir los requisitos de recursos

El enfoque perezoso del ejemplo anterior se enumera a continuación, donde object mb_se crea una instancia en la primera llamada a showMessage(). (Es decir, no hasta que el programa realmente lo necesite).

La clase final pública MyFrame extiende Frame {private MessageBox mb_; // nulo, implícito // auxiliar privado utilizado por esta clase private void showMessage (mensaje de cadena) {if (mb _ == null) // primera llamada a este método mb_ = new MessageBox (); // establece el texto del mensaje mb_.setMessage (mensaje); mb_.pack (); mb_.show (); }}

Si observa más de cerca showMessage(), verá que primero determinamos si la variable de instancia mb_ es igual a nula. Como no hemos inicializado mb_ en su punto de declaración, el tiempo de ejecución de Java se ha encargado de esto por nosotros. Por lo tanto, podemos continuar con seguridad creando la MessageBoxinstancia. Todas las llamadas futuras a showMessage()encontrarán que mb_ no es igual a null, por lo que se omitirá la creación del objeto y se utilizará la instancia existente.

Un ejemplo del mundo real

Examinemos ahora un ejemplo más realista, donde la instanciación perezosa puede jugar un papel clave en la reducción de la cantidad de recursos utilizados por un programa.

Supongamos que un cliente nos ha pedido que escribamos un sistema que permita a los usuarios catalogar imágenes en un sistema de archivos y brindar la posibilidad de ver miniaturas o imágenes completas. Nuestro primer intento podría ser escribir una clase que cargue la imagen en su constructor.

public class ImageFile {private String filename_; Imagen privada image_; public ImageFile (String nombre de archivo) {nombre de archivo_ = nombre de archivo; // carga la imagen} public String getName () {return filename_;} public Image getImage () {return image_; }}

En el ejemplo anterior, ImageFileimplementa un enfoque demasiado ansioso para instanciar el Imageobjeto. A su favor, este diseño garantiza que una imagen estará disponible inmediatamente en el momento de una llamada a getImage(). Sin embargo, esto no solo podría ser terriblemente lento (en el caso de un directorio que contenga muchas imágenes), sino que este diseño podría agotar la memoria disponible. Para evitar estos problemas potenciales, podemos intercambiar los beneficios de rendimiento del acceso instantáneo por un uso reducido de la memoria. Como habrás adivinado, podemos lograr esto usando la instanciación diferida.

Aquí está la ImageFileclase actualizada usando el mismo enfoque que MyFramehizo la clase con su MessageBoxvariable de instancia:

public class ImageFile {private String filename_; Imagen privada image_; // = nulo, implícito public ImageFile (String filename) {// solo almacena el nombre de archivo filename_ = filename; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// primera llamada a getImage () // cargar la imagen ...} return image_; }}

En esta versión, la imagen real se carga solo en la primera llamada a getImage(). Entonces, en resumen, la compensación aquí es que para reducir el uso general de la memoria y los tiempos de inicio, pagamos el precio por cargar la imagen la primera vez que se solicita, lo que introduce un impacto en el rendimiento en ese punto de la ejecución del programa. Este es otro idioma que refleja el Proxypatrón en un contexto que requiere un uso restringido de la 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.

El uso de varios subprocesos en Java puede resultar muy complejo. De hecho, el tema de la concurrencia es tan vasto que Doug Lea ha escrito un libro completo sobre él: Programación concurrente en Java. Si es nuevo en la programación concurrente, le recomendamos que obtenga una copia de este libro antes de embarcarse en la escritura de sistemas Java complejos que se basan en múltiples subprocesos.