Java Tip 67: Leniwe tworzenie instancji

Jeszcze nie tak dawno byliśmy zachwyceni perspektywą posiadania wbudowanej pamięci w 8-bitowym mikrokomputerze, która przeskoczyła z 8 KB do 64 KB. Sądząc po stale rosnących aplikacjach wymagających dużej ilości zasobów, których obecnie używamy, jest zdumiewające, że komukolwiek udało się napisać program, który mieściłby się w tak niewielkiej ilości pamięci. Chociaż w dzisiejszych czasach mamy znacznie więcej pamięci do zabawy, kilka cennych lekcji można wyciągnąć z technik ustanowionych do pracy w tak napiętych ograniczeniach.

Co więcej, programowanie w Javie to nie tylko pisanie apletów i aplikacji do wdrożenia na komputerach osobistych i stacjach roboczych; Java również mocno wkroczyła na rynek systemów wbudowanych. Obecne systemy wbudowane mają stosunkowo ograniczone zasoby pamięci i moc obliczeniową, więc wiele starych problemów, z którymi borykają się programiści, pojawiło się ponownie u programistów Java pracujących w dziedzinie urządzeń.

Równoważenie tych czynników jest fascynującym problemem projektowym: ważne jest, aby zaakceptować fakt, że żadne rozwiązanie w obszarze projektowania osadzonego nie będzie idealne. Musimy więc zrozumieć rodzaje technik, które będą przydatne w osiągnięciu odpowiedniej równowagi wymaganej do pracy w ramach ograniczeń platformy wdrożeniowej.

Jedną z technik oszczędzania pamięci, którą programiści Java uznają za przydatną, jest leniwa instancja. W przypadku leniwego tworzenia instancji program powstrzymuje się od tworzenia pewnych zasobów, dopóki zasób nie jest najpierw potrzebny - zwalniając cenne miejsce w pamięci. W tej wskazówce przyjrzymy się leniwym technikom tworzenia instancji w ładowaniu klas Java i tworzeniu obiektów, a także specjalnym zagadnieniom wymaganym w przypadku wzorców Singleton. Materiał w tej wskazówce pochodzi z pracy w rozdziale 9 naszej książki, Java w praktyce: Design Styles & Idioms for Effective Java (patrz Zasoby).

Chętna kontra leniwa instancja: przykład

Jeśli znasz przeglądarkę internetową Netscape i korzystałeś z obu wersji 3.x i 4.x, z pewnością zauważyłeś różnicę w ładowaniu środowiska wykonawczego Java. Jeśli spojrzysz na ekran powitalny podczas uruchamiania Netscape 3, zauważysz, że ładuje on różne zasoby, w tym Javę. Jednak po uruchomieniu Netscape 4.x nie ładuje on środowiska wykonawczego Java - czeka, aż odwiedzisz stronę WWW zawierającą tag. Te dwa podejścia ilustrują techniki szybkiej instancji (ładuj ją, jeśli jest potrzebna) i leniwej instancji (poczekaj, aż zostanie ona zażądana, zanim ją załadujesz, ponieważ może nigdy nie być potrzebna).

Oba podejścia mają wady: z jednej strony zawsze ładowanie zasobu potencjalnie marnuje cenną pamięć, jeśli zasób nie jest używany podczas tej sesji; z drugiej strony, jeśli nie został załadowany, płacisz cenę w postaci czasu ładowania, gdy zasób jest potrzebny po raz pierwszy.

Rozważ leniwe instancje jako strategię ochrony zasobów

Leniwe tworzenie instancji w Javie można podzielić na dwie kategorie:

  • Leniwe ładowanie zajęć
  • Leniwe tworzenie obiektów

Leniwe ładowanie zajęć

Środowisko wykonawcze Java ma wbudowane opóźnione tworzenie instancji klas. Klasy ładują się do pamięci tylko wtedy, gdy są przywoływane po raz pierwszy. (Mogą być również ładowane najpierw z serwera internetowego za pośrednictwem protokołu HTTP).

MyUtils.classMethod (); // pierwsze wywołanie statycznej metody klasy Vector v = new Vector (); // pierwsze wywołanie operatora new

Leniwe ładowanie klas jest ważną funkcją środowiska wykonawczego Java, ponieważ w pewnych okolicznościach może zmniejszyć zużycie pamięci. Na przykład, jeśli część programu nigdy nie jest wykonywana podczas sesji, klasy, do których odwołuje się tylko ta część programu, nigdy nie zostaną załadowane.

Leniwe tworzenie obiektów

Leniwe tworzenie obiektów jest ściśle powiązane z leniwym ładowaniem klas. Przy pierwszym użyciu słowa kluczowego new w klasie, która wcześniej nie została załadowana, środowisko wykonawcze Java załaduje je za Ciebie. Leniwe tworzenie obiektów może zmniejszyć użycie pamięci w znacznie większym stopniu niż leniwe ładowanie klas.

Aby przedstawić koncepcję leniwego tworzenia obiektów, przyjrzyjmy się prostemu przykładowi kodu, w którym a Frameużywa a MessageBoxdo wyświetlania komunikatów o błędach:

public class MyFrame rozszerza Frame {private MessageBox mb_ = new MessageBox (); // prywatny pomocnik używany przez tę klasę private void showMessage (wiadomość tekstowa) {// ustaw tekst wiadomości mb_.setMessage (wiadomość); mb_.pack (); mb_.show (); }}

W powyższym przykładzie, gdy MyFrametworzona jest instancja programu, tworzona MessageBoxjest również instancja mb_. Te same zasady mają zastosowanie rekurencyjnie. Zatem wszelkie zmienne instancji zainicjowane lub przypisane w MessageBoxkonstruktorze klasy również są przydzielane poza stertę i tak dalej. Jeśli wystąpienie MyFramenie jest używane do wyświetlania komunikatu o błędzie w sesji, niepotrzebnie marnujemy pamięć.

W tym dość prostym przykładzie tak naprawdę nie zyskamy zbyt wiele. Ale jeśli weźmiesz pod uwagę bardziej złożoną klasę, która używa wielu innych klas, które z kolei używają i tworzą więcej obiektów rekurencyjnie, potencjalne użycie pamięci jest bardziej widoczne.

Rozważ leniwe tworzenie instancji jako zasadę zmniejszania wymagań dotyczących zasobów

Leniwe podejście do powyższego przykładu jest wymienione poniżej, gdzie object mb_jest tworzony przy pierwszym wywołaniu showMessage(). (To znaczy dopiero wtedy, gdy jest to rzeczywiście potrzebne programowi).

publiczna klasa końcowa MyFrame rozszerza Frame {private MessageBox mb_; // null, implicit // prywatny pomocnik używany przez tę klasę private void showMessage (String message) {if (mb _ == null) // pierwsze wywołanie tej metody mb_ = new MessageBox (); // ustaw treść wiadomości mb_.setMessage (wiadomość); mb_.pack (); mb_.show (); }}

Jeśli przyjrzysz się bliżej showMessage(), zobaczysz, że najpierw określamy, czy zmienna instancji mb_ jest równa null. Ponieważ nie zainicjowaliśmy mb_ w momencie jego deklaracji, środowisko wykonawcze Java zajęło się tym za nas. W ten sposób możemy bezpiecznie kontynuować tworzenie MessageBoxinstancji. Wszystkie przyszłe wywołania funkcji do showMessage()stwierdzą, że wartość mb_ nie jest równa null, dlatego pomija tworzenie obiektu i używanie istniejącej instancji.

Przykład z prawdziwego świata

Przeanalizujmy teraz bardziej realistyczny przykład, w którym leniwe tworzenie instancji może odgrywać kluczową rolę w zmniejszaniu ilości zasobów używanych przez program.

Załóżmy, że klient poprosił nas o napisanie systemu, który umożliwi użytkownikom katalogowanie obrazów w systemie plików i zapewni możliwość przeglądania miniatur lub całych obrazów. Naszą pierwszą próbą może być napisanie klasy ładującej obraz w swoim konstruktorze.

public class ImageFile {private String nazwa_pliku_; obraz prywatny image_; public ImageFile (String filename) {filename_ = filename; // załaduj obraz} public String getName () {return filename_;} public Image getImage () {return image_; }}

W powyższym przykładzie ImageFileimplementuje przesadne podejście do tworzenia wystąpienia Imageobiektu. Na jego korzyść ten projekt gwarantuje, że obraz będzie dostępny natychmiast w momencie wezwania getImage(). Jednak nie tylko może to być boleśnie powolne (w przypadku katalogu zawierającego wiele obrazów), ale ten projekt może wyczerpać dostępną pamięć. Aby uniknąć tych potencjalnych problemów, możemy zamienić korzyści z wydajności wynikające z natychmiastowego dostępu na mniejsze zużycie pamięci. Jak można się domyślić, możemy to osiągnąć za pomocą leniwej instancji.

Oto zaktualizowana ImageFileklasa wykorzystująca to samo podejście, co klasa MyFrameze swoją MessageBoxzmienną instancji:

public class ImageFile {private String nazwa_pliku_; obraz prywatny image_; // = null, implicit public ImageFile (String filename) {// przechowuj tylko nazwę pliku filename_ = filename; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// pierwsze wywołanie getImage () // załaduj obraz ...} return image_; }}

W tej wersji rzeczywisty obraz jest ładowany tylko przy pierwszym wywołaniu getImage(). Podsumowując, kompromis polega na tym, że aby zmniejszyć całkowite zużycie pamięci i czas uruchamiania, płacimy cenę za załadowanie obrazu przy pierwszym żądaniu - wprowadzając wydajność osiągniętą w tym momencie wykonywania programu. To kolejny idiom, który odzwierciedla Proxywzorzec w kontekście, który wymaga ograniczonego użycia pamięci.

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.

Korzystanie z wielu wątków w Javie może być bardzo złożone. W rzeczywistości temat współbieżności jest tak rozległy, że Doug Lea napisał o nim całą książkę: Programowanie współbieżne w Javie. Jeśli nie masz doświadczenia w programowaniu współbieżnym, zalecamy zaopatrzenie się w kopię tej książki, zanim zaczniesz pisać złożone systemy Java, które są oparte na wielu wątkach.