Javaのヒント67:遅延インスタンス化

8ビットマイクロコンピュータのオンボードメモリが8KBから64KBにジャンプするという見通しに興奮したのは、それほど昔のことではありません。現在使用しているリソースを大量に消費するアプリケーションが増え続けていることから判断すると、そのわずかな量のメモリに収まるようにプログラムを作成できたのは驚くべきことです。最近はもっとたくさんの記憶がありますが、そのような厳しい制約の中で機能するように確立されたテクニックから、いくつかの貴重な教訓を学ぶことができます。

さらに、Javaプログラミングは、パーソナルコンピュータやワークステーションに展開するためのアプレットやアプリケーションを作成するだけではありません。Javaは、組み込みシステム市場にも強力に参入しています。現在の組み込みシステムは、メモリリソースと計算能力が比較的不足しているため、プログラマーが直面している古い問題の多くが、デバイス領域で作業するJava開発者にとって再浮上しています。

これらの要素のバランスを取ることは、魅力的な設計上の問題です。組み込み設計の分野では完璧な解決策はないという事実を受け入れることが重要です。したがって、展開プラットフォームの制約内で機能するために必要な微妙なバランスを実現するのに役立つテクニックの種類を理解する必要があります。

Javaプログラマーが役立つと考えるメモリ節約手法の1つは、遅延インスタンス化です。遅延インスタンス化により、プログラムは、リソースが最初に必要になるまで特定のリソースの作成を控え、貴重なメモリスペースを解放します。このヒントでは、Javaクラスのロードとオブジェクトの作成における遅延インスタンス化手法と、シングルトンパターンに必要な特別な考慮事項について説明します。このヒントの内容は、私たちの本の第9章「Javaの実践:効果的なJavaの設計スタイルとイディオム」(「参考文献」を参照)の作業から派生しています。

熱心なインスタンス化と怠惰なインスタンス化:例

NetscapeのWebブラウザに精通していて、バージョン3.xと4.xの両方を使用したことがある場合は、間違いなく、Javaランタイムのロード方法に違いがあることに気づいたでしょう。 Netscape 3の起動時にスプラッシュ画面を見ると、Javaを含むさまざまなリソースが読み込まれていることがわかります。ただし、Netscape 4.xを起動すると、Javaランタイムはロードされません。タグを含むWebページにアクセスするまで待機します。これらの2つのアプローチは、熱心なインスタンス化(必要な場合にロードする)と怠惰なインスタンス化(要求されるまで待ってからロードする)の手法を示しています

両方のアプローチには欠点があります。一方で、リソースがそのセッション中に使用されない場合、常にリソースをロードすると貴重なメモリが浪費される可能性があります。一方、ロードされていない場合は、リソースが最初に必要になったときにロード時間の観点から料金を支払います。

怠惰なインスタンス化をリソース保護ポリシーとして検討する

Javaでのレイジーインスタンス化は、次の2つのカテゴリに分類されます。

  • 怠惰なクラスの読み込み
  • 怠惰なオブジェクトの作成

怠惰なクラスの読み込み

Javaランタイムには、クラスの遅延インスタンス化が組み込まれています。クラスは、最初に参照されたときにのみメモリにロードされます。(最初にHTTP経由でWebサーバーからロードすることもできます。)

MyUtils.classMethod(); //静的クラスメソッドの最初の呼び出しVectorv = new Vector(); //演算子newの最初の呼び出し

レイジークラスロードは、特定の状況下でメモリ使用量を削減できるため、Javaランタイム環境の重要な機能です。たとえば、プログラムの一部がセッション中に実行されない場合、プログラムのその部分でのみ参照されるクラスがロードされることはありません。

怠惰なオブジェクトの作成

レイジーオブジェクトの作成は、レイジークラスのロードと密接に関連しています。以前にロードされていないクラスタイプで初めてnewキーワードを使用すると、Javaランタイムがそれをロードします。レイジーオブジェクトの作成は、レイジークラスのロードよりもはるかにメモリ使用量を減らすことができます。

レイジーオブジェクト作成の概念を紹介するために、aをFrame使用しMessageBoxてエラーメッセージを表示する簡単なコード例を見てみましょう。

public class MyFrame extends Frame {private MessageBox mb_ = new MessageBox(); //このクラスで使用されるプライベートヘルパーprivatevoid showMessage(String message){//メッセージテキストを設定しますmb_.setMessage(message); mb_.pack(); mb_.show(); }}

上記の例でMyFrameは、のMessageBoxインスタンスが作成されると、インスタンスmb_も作成されます。同じルールが再帰的に適用されます。したがって、クラスMessageBoxのコンストラクタで初期化または割り当てられたインスタンス変数も、ヒープから割り当てられます。のインスタンスがMyFrameセッション内でエラーメッセージを表示するために使用されていない場合、メモリを不必要に浪費しています。

このかなり単純な例では、実際にはあまり多くを得るつもりはありません。しかし、他の多くのクラスを使用し、さらに多くのオブジェクトを再帰的に使用してインスタンス化する、より複雑なクラスを検討すると、潜在的なメモリ使用量がより明確になります。

リソース要件を削減するためのポリシーとして、遅延インスタンス化を検討してください

上記の例への怠惰なアプローチを以下に示します。ここでobject mb_は、の最初の呼び出しでがインスタンス化されshowMessage()ます。(つまり、プログラムで実際に必要になるまでは。)

public final class MyFrame extends Frame {private MessageBox mb_; // null、暗黙的//このクラスで使用されるプライベートヘルパーprivate void showMessage(String message){if(mb _ == null)//このメソッドの最初の呼び出しmb_ = new MessageBox(); //メッセージテキストを設定しますmb_.setMessage(message); mb_.pack(); mb_.show(); }}

を詳しく見るとshowMessage()、最初にインスタンス変数mb_がnullに等しいかどうかを判断していることがわかります。宣言の時点でmb_を初期化していないため、Javaランタイムがこれを処理してくれます。したがって、MessageBoxインスタンスを作成することで安全に進めることができます。今後のすべての呼び出しでshowMessage()は、mb_がnullに等しくないことが検出されるため、オブジェクトの作成をスキップして既存のインスタンスを使用します。

実際の例

ここで、より現実的な例を見てみましょう。レイジーインスタンス化は、プログラムで使用されるリソースの量を削減する上で重要な役割を果たすことができます。

クライアントから、ユーザーがファイルシステム上の画像をカタログ化し、サムネイルまたは完全な画像を表示する機能を提供するシステムを作成するように依頼されたとします。最初の試みは、コンストラクターに画像をロードするクラスを作成することかもしれません。

パブリッククラスImageFile {プライベート文字列ファイル名_; プライベート画像image_; public ImageFile(String filename){filename_ = filename; //画像をロードします} public String getName(){return filename_;} public Image getImage(){return image_; }}

上記の例でImageFileは、Imageオブジェクトをインスタンス化するための過度のアプローチを実装しています。この設計により、への呼び出し時に画像がすぐに利用可能になることが保証されgetImage()ます。ただし、これは非常に遅くなる可能性があるだけでなく(多くの画像を含むディレクトリの場合)、この設計は使用可能なメモリを使い果たす可能性があります。これらの潜在的な問題を回避するために、瞬時アクセスのパフォーマンス上の利点をメモリ使用量の削減と交換することができます。ご想像のとおり、レイジーインスタンス化を使用することでこれを実現できます。

これImageFileは、クラスMyFrameMessageBoxインスタンス変数で行ったのと同じアプローチを使用して更新されたクラスです。

public class ImageFile {private String filename_; プライベート画像image_; // = null、暗黙のpublic ImageFile(String filename){//ファイル名のみを保存filename_ = filename; } public String getName(){return filename_;} public Image getImage(){if(image _ == null){// getImage()の最初の呼び出し//画像を読み込む...} return image_; }}

このバージョンでは、実際の画像はgetImage()。への最初の呼び出しでのみロードされます。要約すると、ここでのトレードオフは、全体的なメモリ使用量と起動時間を削減するために、最初に要求されたときにイメージをロードするための代償を支払うことです。つまり、プログラムの実行のその時点でパフォーマンスが低下します。これはProxy、メモリの制約された使用を必要とするコンテキストでパターンを反映する別のイディオムです。

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で複数のスレッドを使用することは、非常に複雑になる可能性があります。実際、並行性のトピックは非常に広大であるため、DougLeaはそれに関する本全体を書いています:Javaでの並行プログラミング。並行プログラミングに慣れていない場合は、複数のスレッドに依存する複雑なJavaシステムの作成に着手する前に、この本のコピーを入手することをお勧めします。