一見単純なシングルトンパターンをナビゲートする方法

シングルトンパターンは、特にJava開発者にとってさえ、一見シンプルです。この古典的なJavaWorldの記事では、David Gearyが、Java開発者がシングルトンを実装する方法を示し、マルチスレッド、クラスローダー、およびシングルトンパターンを使用したシリアル化のコード例を示します。彼は、実行時にシングルトンを指定するためにシングルトンレジストリを実装することを検討して締めくくります。

クラスのインスタンスを1つだけ持つことが適切な場合があります。ウィンドウマネージャー、印刷スプーラー、ファイルシステムは典型的な例です。通常、シングルトンと呼ばれるこれらのタイプのオブジェクトは、ソフトウェアシステム全体の異なるオブジェクトによってアクセスされるため、グローバルなアクセスポイントが必要です。もちろん、複数のインスタンスが必要になることはないと確信している場合は、考えを変えることをお勧めします。

シングルトンデザインパターンは、これらすべての懸念に対処します。シングルトンデザインパターンを使用すると、次のことができます。

  • クラスのインスタンスが1つだけ作成されていることを確認します
  • オブジェクトへのグローバルアクセスポイントを提供します
  • シングルトンクラスのクライアントに影響を与えることなく、将来的に複数のインスタンスを許可する

シングルトンデザインパターンは、下の図に示されているように、最も単純なデザインパターンの1つですが、不注意なJava開発者にとっては多くの落とし穴があります。この記事では、シングルトンデザインパターンについて説明し、それらの落とし穴に対処します。

Javaデザインパターンの詳細

David GearyのJavaデザインパターンの列をすべて読んだり、Javaデザインパターンに関するJavaWorldの最新の記事のリストを表示したりできます。Gang of Fourパターンを使用することの長所と短所についてはデザインパターン、全体像」を参照してください。もっと欲しい?EnterpriseJavaニュースレターを受信トレイに配信します。

シングルトンパターン

デザインパターン:オブジェクト指向における再利用のための、四つのギャングは、このようなシングルトンパターンを説明します。

クラスにインスタンスが1つしかないことを確認し、そのクラスへのグローバルアクセスポイントを提供します。

次の図は、シングルトンデザインパターンのクラス図を示しています。

ご覧のとおり、シングルトンデザインパターンにはそれほど多くはありません。シングルトンは、唯一のシングルトンインスタンスへの静的参照を維持し、静的instance()メソッドからそのインスタンスへの参照を返します。

例1は、古典的なシングルトンデザインパターンの実装を示しています。

例1.古典的なシングルトン

public class ClassicSingleton { private static ClassicSingleton instance = null; protected ClassicSingleton() { // Exists only to defeat instantiation. } public static ClassicSingleton getInstance() { if(instance == null) { instance = new ClassicSingleton(); } return instance; } }

例1で実装されたシングルトンは理解しやすいです。このClassicSingletonクラスは、唯一のシングルトンインスタンスへの静的参照を維持し、静的getInstance()メソッドからその参照を返します。

ClassicSingletonクラスに関していくつかの興味深い点があります。まず、レイジーインスタンス化ClassicSingleton呼ばれる手法を使用してシングルトンを作成します。その結果、メソッドが最初に呼び出されるまで、シングルトンインスタンスは作成されません。この手法により、必要な場合にのみシングルトンインスタンスが作成されます。getInstance()

次に、ClassicSingletonクライアントがインスタンスをClassicSingletonインスタンス化できないように、保護されたコンストラクターを実装していることに注意してください。ただし、次のコードが完全に合法であることに驚かれるかもしれません。

public class SingletonInstantiator { public SingletonInstantiator() { ClassicSingleton instance = ClassicSingleton.getInstance(); ClassicSingleton anotherInstance =new ClassicSingleton(); ... } }

コンストラクターが保護されている場合、上記のコードフラグメント(拡張されていない)のクラスはどのようにしClassicSingletonClassicSingletonインスタンスを作成できClassicSingletonますか?答えは、保護されたコンストラクターは、サブクラスおよび同じパッケージ内の他のクラスから呼び出すことができるということです。のでClassicSingletonSingletonInstantiator同じパッケージ(デフォルトパッケージ)であり、SingletonInstantiator()方法は作成することができClassicSingletonインスタンスを。このジレンマには2つの解決策があります。ClassicSingletonコンストラクターをプライベートにして、ClassicSingleton()メソッドのみが呼び出すようにすることができます。ただし、それはClassicSingletonサブクラス化できないことを意味します。時には、それが望ましい解決策です。もしそうなら、あなたのシングルトンクラスを宣言するのは良い考えですfinal、これにより、その意図が明確になり、コンパイラーがパフォーマンスの最適化を適用できるようになります。他の解決策は、シングルトンクラスを明示的なパッケージに入れることです。そのため、他のパッケージ(デフォルトパッケージを含む)のクラスはシングルトンインスタンスをインスタンス化できません。

についての3番目の興味深い点ClassicSingleton:異なるクラスローダーによってロードされたクラスがシングルトンにアクセスする場合、複数のシングルトンインスタンスを持つことが可能です。そのシナリオはそれほど先取りされていません。たとえば、一部のサーブレットコンテナは、サーブレットごとに異なるクラスローダーを使用するため、2つのサーブレットがシングルトンにアクセスする場合、それぞれに独自のインスタンスがあります。

第4に、インターフェースをClassicSingleton実装する場合java.io.Serializable、クラスのインスタンスをシリアル化および逆シリアル化できます。ただし、シングルトンオブジェクトをシリアル化し、その後そのオブジェクトを複数回逆シリアル化すると、複数のシングルトンインスタンスが作成されます。

最後に、おそらく最も重要なのは、例1のClassicSingletonクラスはスレッドセーフではないということです。2つのスレッド(スレッド1とスレッド2と呼びます)がClassicSingleton.getInstance()同時に呼び出すClassicSingleton場合、スレッド1がifブロックに入った直後にプリエンプトされ、その後スレッド2に制御が与えられると、2つのインスタンスが作成されます。

前の説明からわかるように、シングルトンパターンは最も単純なデザインパターンの1つですが、Javaでの実装は単純ではありません。この記事の残りの部分では、シングルトンパターンに関するJava固有の考慮事項について説明しますが、最初に少し回り道をして、シングルトンクラスをテストする方法を見てみましょう。

シングルトンをテストする

この記事の残りの部分では、log4jと連携してJUnitを使用してシングルトンクラスをテストします。JUnitまたはlog4jに精通していない場合は、「参考文献」を参照してください。

例2は、例1のシングルトンをテストするJUnitテストケースを示しています。

例2.シングルトンテストケース

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) { super(name); } public void setUp() { logger.info("getting singleton..."); sone = ClassicSingleton.getInstance(); logger.info("...got singleton: " + sone); logger.info("getting singleton..."); stwo = ClassicSingleton.getInstance(); logger.info("...got singleton: " + stwo); } public void testUnique() { logger.info("checking singletons for equality"); Assert.assertEquals(true, sone == stwo); } }

例2のテストケースはClassicSingleton.getInstance()2回呼び出し、返された参照をメンバー変数に格納します。このtestUnique()メソッドは、参照が同一であることを確認します。例3は、テストケースの出力を示しています。

Example 3. Test case output

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) compile: run-test-text: [java] .INFO main: getting singleton... [java] INFO main: created singleton: [email protected] [java] INFO main: ...got singleton: [email protected] [java] INFO main: getting singleton... [java] INFO main: ...got singleton: [email protected] [java] INFO main: checking singletons for equality [java] Time: 0.032 [java] OK (1 test)

As the preceding listing illustrates, Example 2's simple test passes with flying colors—the two singleton references obtained with ClassicSingleton.getInstance() are indeed identical; however, those references were obtained in a single thread. The next section stress-tests our singleton class with multiple threads.

Multithreading considerations

Example 1's ClassicSingleton.getInstance() method is not thread-safe because of the following code:

1: if(instance == null) { 2: instance = new Singleton(); 3: }

If a thread is preempted at Line 2 before the assignment is made, the instance member variable will still be null, and another thread can subsequently enter the if block. In that case, two distinct singleton instances will be created. Unfortunately, that scenario rarely occurs and is therefore difficult to produce during testing. To illustrate this thread Russian roulette, I've forced the issue by reimplementing Example 1's class. Example 4 shows the revised singleton class:

Example 4. Stack the deck

import org.apache.log4j.Logger; public class Singleton { private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger(); private static boolean firstThread = true; protected Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; } private static void simulateRandomActivity() { try { if(firstThread) { firstThread = false; logger.info("sleeping..."); // This nap should give the second thread enough time // to get by the first thread.Thread.currentThread().sleep(50); } } catch(InterruptedException ex) { logger.warn("Sleep interrupted"); } } }

Example 4's singleton resembles Example 1's class, except the singleton in the preceding listing stacks the deck to force a multithreading error. The first time the getInstance() method is called, the thread that invoked the method sleeps for 50 milliseconds, which gives another thread time to call getInstance() and create a new singleton instance. When the sleeping thread awakes, it also creates a new singleton instance, and we have two singleton instances. Although Example 4's class is contrived, it stimulates the real-world situation where the first thread that calls getInstance() gets preempted.

Example 5 tests Example 4's singleton:

Example 5. A test that fails

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private static Logger logger = Logger.getRootLogger(); private static Singleton singleton = null; public SingletonTest(String name) { super(name); } public void setUp() { singleton = null; } public void testUnique() throws InterruptedException { // Both threads call Singleton.getInstance(). Thread threadOne = new Thread(new SingletonTestRunnable()), threadTwo = new Thread(new SingletonTestRunnable()); threadOne.start();threadTwo.start(); threadOne.join(); threadTwo.join(); } private static class SingletonTestRunnable implements Runnable { public void run() { // Get a reference to the singleton. Singleton s = Singleton.getInstance(); // Protect singleton member variable from // multithreaded access. synchronized(SingletonTest.class) { if(singleton == null) // If local reference is null... singleton = s; // ...set it to the singleton } // Local reference must be equal to the one and // only instance of Singleton; otherwise, we have two // Singleton instances. Assert.assertEquals(true, s == singleton); } } }

Example 5's test case creates two threads, starts each one, and waits for them to finish. The test case maintains a static reference to a singleton instance, and each thread calls Singleton.getInstance(). If the static member variable has not been set, the first thread sets it to the singleton obtained with the call to getInstance(), and the static member variable is compared to the local variable for equality.

テストケースが実行されるとgetInstance()、次のifようになります。最初のスレッドが呼び出し、ブロックに入り、スリープします。続いて、2番目のスレッドもgetInstance()シングルトンインスタンスを呼び出して作成します。次に、2番目のスレッドは、静的メンバー変数を作成したインスタンスに設定します。2番目のスレッドは、静的メンバー変数とローカルコピーが等しいかどうかをチェックし、テストに合格します。最初のスレッドが起動すると、シングルトンインスタンスも作成されますが、そのスレッドは静的メンバー変数を設定しないため(2番目のスレッドはすでに設定しているため)、静的変数とローカル変数は同期していません。平等のために失敗します。例6に、例5のテストケース出力を示します。