Java Съвет 67: Мързеливи инстанции

Не толкова отдавна бяхме развълнувани от перспективата да имаме вградена памет в 8-битов микрокомпютър, скочил от 8 KB на 64 KB. Съдейки по непрекъснато нарастващите, жадни за ресурси приложения, които сега използваме, невероятно е, че някой някога е успял да напише програма, която да се побере в това малко количество памет. Въпреки че имаме много повече памет, с която да играем в наши дни, някои ценни уроци могат да бъдат извлечени от техниките, установени за работа в такива строги ограничения.

Освен това Java програмирането не е само писане на аплети и приложения за разполагане на персонални компютри и работни станции; Java направи силен пробив и на пазара на вградени системи. Текущите вградени системи имат относително оскъдни ресурси памет и изчислителна мощ, така че много от старите проблеми, пред които са изправени програмистите, се появиха отново за разработчиците на Java, работещи в сферата на устройството.

Балансирането на тези фактори е завладяващ дизайнерски проблем: Важно е да приемете факта, че нито едно решение в областта на вградения дизайн няма да бъде перфектно. И така, трябва да разберем видовете техники, които ще бъдат полезни за постигане на финия баланс, необходим за работа в рамките на ограниченията на платформата за внедряване.

Една от техниките за запазване на паметта, която Java програмистите намират за полезна, е мързеливата инстанция. С мързелива инстанция, програма се въздържа от създаването на определени ресурси, докато ресурсът е необходим за първи път - освобождавайки ценно място в паметта. В този съвет ние разглеждаме лениви техники за създаване на екземпляри при зареждане на Java клас и създаване на обекти, както и специалните съображения, необходими за модели на Singleton. Материалът в този съвет произтича от работата в глава 9 на нашата книга, Java на практика: Стилове на проектиране и идиоми за ефективна Java (вж. Ресурси).

Нетърпелив срещу мързелив пример: пример

Ако сте запознати с уеб браузъра на Netscape и сте използвали и двете версии 3.x и 4.x, несъмнено сте забелязали разлика в начина на зареждане на Java Runtime. Ако погледнете началния екран, когато Netscape 3 се стартира, ще забележите, че той зарежда различни ресурси, включително Java. Въпреки това, когато стартирате Netscape 4.x, той не зарежда времето за изпълнение на Java - изчаква, докато посетите уеб страница, която включва маркера. Тези два подхода илюстрират техниките на нетърпелива инстанция (заредете я в случай, че е необходима) и мързелива инстанция (изчакайте, докато не бъде поискана, преди да я заредите, тъй като може никога да не е необходима).

И при двата подхода има недостатъци: От една страна, винаги зареждането на ресурс потенциално губи ценна памет, ако ресурсът не се използва по време на тази сесия; от друга страна, ако не е зареден, плащате цената по отношение на времето за зареждане, когато ресурсът се изисква за първи път.

Помислете за мързеливата инстанция като политика за опазване на ресурсите

Мързеливият екземпляр в Java се разделя на две категории:

  • Мързеливо зареждане на клас
  • Мързеливо създаване на обект

Мързеливо зареждане на клас

Времетраенето на Java има вградена мързелива инстанция за класове. Класовете се зареждат в паметта само когато за първи път са посочени. (Те също могат да бъдат заредени първо от уеб сървър чрез HTTP.)

MyUtils.classMethod (); // първо извикване на метод на статичен клас Vector v = new Vector (); // първо обаждане до оператора new

Мързеливото зареждане на клас е важна характеристика на средата за изпълнение на Java, тъй като може да намали използването на памет при определени обстоятелства. Например, ако част от програма никога не се изпълнява по време на сесия, класовете, посочени само в тази част на програмата, никога няма да бъдат заредени.

Мързеливо създаване на обект

Създаването на мързеливи обекти е тясно свързано с мързеливо зареждане на клас. Първият път, когато използвате новата ключова дума за тип клас, който преди това не е бил зареден, изпълнението на Java ще го зареди вместо вас. Създаването на мързеливи обекти може да намали използването на памет в много по-голяма степен, отколкото мързеливото зареждане на клас.

За да представим концепцията за мързеливо създаване на обекти, нека разгледаме прост пример на код, където a Frameизползва a MessageBoxза показване на съобщения за грешка:

публичен клас MyFrame разширява Frame {private MessageBox mb_ = new MessageBox (); // частен помощник, използван от този клас private void showMessage (String message) {// задайте текста на съобщението mb_.setMessage (message); mb_.pack (); mb_.show (); }}

В горния пример, когато се създава екземпляр на, MyFrameсе създава и MessageBoxинстанцията mb_. Същите правила се прилагат рекурсивно. Така че всички променливи на екземпляра, инициализирани или присвоени в MessageBoxконструктора на класа , също се разпределят извън купчината и т.н. Ако екземплярът на MyFrameне се използва за показване на съобщение за грешка в рамките на сесия, губим памет ненужно.

В този доста прост пример ние наистина няма да спечелим твърде много. Но ако помислите за по-сложен клас, който използва много други класове, които от своя страна използват и инстанцират повече обекти рекурсивно, потенциалното използване на паметта е по-очевидно.

Помислете за мързеливата инстанция като политика за намаляване на изискванията за ресурси

Мързеливият подход към горния пример е изброен по-долу, където object mb_инстанцията е създадена при първото извикване на showMessage(). (Тоест, докато действително не е необходимо на програмата.)

публичен финален клас MyFrame разширява Frame {private MessageBox mb_; // нула, имплицитен // частен помощник, използван от този клас private void showMessage (String message) {if (mb _ == null) // първо извикване на този метод mb_ = new MessageBox (); // задаване на текст на съобщението mb_.setMessage (съобщение); mb_.pack (); mb_.show (); }}

Ако погледнете отблизо showMessage(), ще видите, че първо определяме дали променливата на екземпляра mb_ е равна на null. Тъй като не сме инициализирали mb_ в точката на деклариране, Java Runtime се е погрижил за това вместо нас. По този начин можем спокойно да продължим, като създадем MessageBoxекземпляра. Всички бъдещи извиквания към showMessage()ще открият, че mb_ не е равно на null, поради което пропуска създаването на обекта и използва съществуващия екземпляр.

Пример от реалния свят

Нека сега разгледаме един по-реалистичен пример, където мързеливата инстанция може да играе ключова роля за намаляване на количеството ресурси, използвани от програма.

Да предположим, че клиентът ни е помолен да напишем система, която ще позволи на потребителите да каталогизират изображения във файлова система и да предоставят възможност за преглед на миниатюри или пълни изображения. Първият ни опит може да бъде да напишем клас, който зарежда изображението в неговия конструктор.

публичен клас ImageFile {частен низ filename_; частно изображение image_; публичен ImageFile (низово име на файл) {filename_ = име на файл; // зареждаме изображението} публичен низ getName () {върнете име_на_файл}} публично изображение getImage () {върнете изображение_; }}

В примера по-горе ImageFileреализира подход за прекомерно използване на екземпляр на Imageобекта. В негова полза този дизайн гарантира, че изображението ще бъде достъпно веднага по време на обаждането getImage(). Това обаче не само може да бъде болезнено бавно (в случай на директория, съдържаща много изображения), но този дизайн може да изчерпи наличната памет. За да избегнем тези потенциални проблеми, можем да разменяме предимствата на производителността на моменталния достъп за намалено използване на паметта. Както може би се досещате, можем да постигнем това, като използваме мързелива инстанция.

Ето актуализирания ImageFileклас, използващ същия подход като клас MyFrameс MessageBoxпроменливата на неговия екземпляр:

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.

Използването на множество нишки в Java може да бъде много сложно. Всъщност темата за едновременността е толкова обширна, че Дъг Леа е написал цяла книга по нея: Паралелно програмиране в Java. Ако не сте запознати с едновременното програмиране, препоръчваме ви да получите копие на тази книга, преди да се впуснете в писането на сложни Java системи, които разчитат на множество нишки.