Astuce Java 67: Instanciation paresseuse

Il n'y a pas si longtemps, nous étions ravis à l'idée d'avoir la mémoire embarquée dans un micro-ordinateur 8 bits passer de 8 Ko à 64 Ko. À en juger par les applications de plus en plus gourmandes en ressources que nous utilisons maintenant, il est étonnant que quiconque ait jamais réussi à écrire un programme pour tenir dans cette petite quantité de mémoire. Bien que nous ayons beaucoup plus de mémoire à jouer ces jours-ci, des leçons précieuses peuvent être tirées des techniques établies pour travailler dans des contraintes aussi strictes.

De plus, la programmation Java ne consiste pas seulement à écrire des applets et des applications à déployer sur des ordinateurs personnels et des postes de travail; Java a également fait des percées importantes sur le marché des systèmes embarqués. Les systèmes embarqués actuels ont des ressources mémoire et une puissance de calcul relativement limitées, de sorte que bon nombre des anciens problèmes auxquels sont confrontés les programmeurs ont refait surface pour les développeurs Java travaillant dans le domaine des périphériques.

Équilibrer ces facteurs est un problème de conception fascinant: il est important d'accepter le fait qu'aucune solution dans le domaine de la conception embarquée ne sera parfaite. Nous devons donc comprendre les types de techniques qui vont être utiles pour atteindre le juste équilibre nécessaire pour travailler dans les contraintes de la plate-forme de déploiement.

L'une des techniques de conservation de la mémoire que les programmeurs Java trouvent utiles est l' instanciation paresseuse. Avec l'instanciation paresseuse, un programme s'abstient de créer certaines ressources jusqu'à ce que la ressource soit d'abord nécessaire, libérant ainsi un espace mémoire précieux. Dans cette astuce, nous examinons les techniques d'instanciation paresseuse dans le chargement de classe Java et la création d'objets, ainsi que les considérations spéciales requises pour les modèles Singleton. Le matériel dans cette astuce dérive du travail dans le chapitre 9 de notre livre, Java en pratique: Styles de conception et idiomes pour Java efficace (voir Ressources).

Instanciation désireuse vs paresseuse: un exemple

Si vous êtes familier avec le navigateur Web de Netscape et avez utilisé les deux versions 3.x et 4.x, vous avez sans aucun doute remarqué une différence dans la façon dont le runtime Java est chargé. Si vous regardez l'écran de démarrage au démarrage de Netscape 3, vous remarquerez qu'il charge diverses ressources, y compris Java. Cependant, lorsque vous démarrez Netscape 4.x, il ne charge pas l'environnement d'exécution Java - il attend que vous visitiez une page Web contenant la balise. Ces deux approches illustrent les techniques d' instanciation hâtive (chargez-la si nécessaire) et d' instanciation paresseuse (attendez qu'elle soit demandée avant de la charger, car elle ne sera peut-être jamais nécessaire).

Il y a des inconvénients aux deux approches: d'une part, toujours charger une ressource gaspille potentiellement une mémoire précieuse si la ressource n'est pas utilisée pendant cette session; par contre, s'il n'a pas été chargé, vous payez le prix en termes de temps de chargement lorsque la ressource est requise pour la première fois.

Considérez l'instanciation paresseuse comme une politique de conservation des ressources

L'instanciation paresseuse en Java se divise en deux catégories:

  • Chargement de classe paresseux
  • Création d'objets paresseux

Chargement de classe paresseux

Le runtime Java a une instanciation différée intégrée pour les classes. Les classes se chargent en mémoire uniquement lorsqu'elles sont référencées pour la première fois. (Ils peuvent également être chargés à partir d'un serveur Web via HTTP d'abord.)

MyUtils.classMethod (); // premier appel à une méthode de classe statique Vector v = new Vector (); // premier appel à l'opérateur new

Le chargement de classe différé est une fonctionnalité importante de l'environnement d'exécution Java car il peut réduire l'utilisation de la mémoire dans certaines circonstances. Par exemple, si une partie d'un programme n'est jamais exécutée pendant une session, les classes référencées uniquement dans cette partie du programme ne seront jamais chargées.

Création d'objets paresseux

La création d'objets paresseux est étroitement liée au chargement de classe paresseux. La première fois que vous utilisez le nouveau mot-clé sur un type de classe qui n'a pas été chargé auparavant, le runtime Java le chargera pour vous. La création d'objets différés peut réduire l'utilisation de la mémoire dans une bien plus grande mesure que le chargement de classes différées.

Pour introduire le concept de création d'objet paresseux, jetons un coup d'œil à un exemple de code simple où a Frameutilise a MessageBoxpour afficher les messages d'erreur:

classe publique MyFrame étend Frame {Private MessageBox mb_ = new MessageBox (); // assistant privé utilisé par cette classe private void showMessage (String message) {// définir le texte du message mb_.setMessage (message); mb_.pack (); mb_.show (); }}

Dans l'exemple ci-dessus, lorsqu'une instance de MyFrameest créée, l' MessageBoxinstance mb_ est également créée. Les mêmes règles s'appliquent récursivement. Ainsi, toutes les variables d'instance initialisées ou affectées dans MessageBoxle constructeur de la classe sont également allouées hors du tas et ainsi de suite. Si l'instance de MyFramen'est pas utilisée pour afficher un message d'erreur dans une session, nous gaspillons de la mémoire inutilement.

Dans cet exemple assez simple, nous n'allons pas vraiment gagner trop. Mais si vous considérez une classe plus complexe, qui utilise de nombreuses autres classes, qui à leur tour utilisent et instancient plus d'objets de manière récursive, l'utilisation potentielle de la mémoire est plus apparente.

Considérez l'instanciation paresseuse comme stratégie pour réduire les besoins en ressources

L'approche paresseuse de l'exemple ci-dessus est répertoriée ci-dessous, où le object mb_est instancié lors du premier appel à showMessage(). (Autrement dit, pas avant que le programme n'en ait réellement besoin.)

classe finale publique MyFrame étend Frame {private MessageBox mb_; // null, implicite // assistant privé utilisé par cette classe private void showMessage (String message) {if (mb _ == null) // premier appel à cette méthode mb_ = new MessageBox (); // définit le texte du message mb_.setMessage (message); mb_.pack (); mb_.show (); }}

Si vous regardez de plus près showMessage(), vous verrez que nous déterminons d'abord si la variable d'instance mb_ est égale à null. Comme nous n'avons pas initialisé mb_ à son point de déclaration, le runtime Java s'est occupé de cela pour nous. Ainsi, nous pouvons procéder en toute sécurité en créant l' MessageBoxinstance. Tous les futurs appels à showMessage()trouveront que mb_ n'est pas égal à null, ignorant ainsi la création de l'objet et utilisant l'instance existante.

Un exemple concret

Examinons maintenant un exemple plus réaliste, où l'instanciation paresseuse peut jouer un rôle clé dans la réduction de la quantité de ressources utilisées par un programme.

Supposons qu'un client nous ait demandé d'écrire un système qui permettra aux utilisateurs de cataloguer des images sur un système de fichiers et de fournir la possibilité d'afficher des miniatures ou des images complètes. Notre première tentative pourrait être d'écrire une classe qui charge l'image dans son constructeur.

classe publique ImageFile {private String filename_; Image privée image_; public ImageFile (String filename) {filename_ = filename; // charge l'image} public String getName () {return filename_;} public Image getImage () {return image_; }}

Dans l'exemple ci-dessus, ImageFileimplémente une approche excessive pour instancier l' Imageobjet. En sa faveur, cette conception garantit qu'une image sera disponible immédiatement au moment d'un appel à getImage(). Cependant, non seulement cela pourrait être extrêmement lent (dans le cas d'un répertoire contenant de nombreuses images), mais cette conception pourrait épuiser la mémoire disponible. Pour éviter ces problèmes potentiels, nous pouvons échanger les avantages de performance de l'accès instantané contre une utilisation réduite de la mémoire. Comme vous l'avez peut-être deviné, nous pouvons y parvenir en utilisant l'instanciation paresseuse.

Voici la ImageFileclasse mise à jour en utilisant la même approche que la classe a MyFramefait avec sa MessageBoxvariable d'instance:

classe publique ImageFile {chaîne privée nom_fichier_; Image privée image_; // = null, implicite public ImageFile (String filename) {// stocke uniquement le nom de fichier filename_ = filename; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// premier appel à getImage () // charge l'image ...} return image_; }}

Dans cette version, l'image réelle est chargée uniquement lors du premier appel à getImage(). Donc, pour récapituler, le compromis ici est que pour réduire l'utilisation globale de la mémoire et les temps de démarrage, nous payons le prix du chargement de l'image la première fois qu'elle est demandée - introduisant un impact sur les performances à ce stade de l'exécution du programme. C'est un autre idiome qui reflète le Proxymodèle dans un contexte qui nécessite une utilisation contrainte de la mémoire.

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'utilisation de plusieurs threads en Java peut être très complexe. En fait, le sujet de la concurrence est si vaste que Doug Lea a écrit un livre entier dessus: la programmation simultanée en Java. Si vous débutez dans la programmation simultanée, nous vous recommandons d'obtenir une copie de ce livre avant de vous lancer dans l'écriture de systèmes Java complexes qui reposent sur plusieurs threads.