Java Singletonデザインパターンのベストプラクティスと例

導入

Javaシングルトンパターンは、四人組のデザインパターンの一つであり、生成デザインパターンのカテゴリに含まれます。定義からすると、これは単純なデザインパターンのようですが、実装に関しては多くの懸念があります。

この記事では、シングルトンデザインパターンの原則について学び、シングルトンデザインパターンを実装する異なる方法や、その使用のベストプラクティスについて探求します。

シングルトンパターンの原則

  • シングルトンパターンは、クラスのインスタンス化を制限し、Java仮想マシン内にクラスの唯一のインスタンスが存在することを保証します。
  • シングルトンクラスは、クラスのインスタンスを取得するためのグローバルアクセスポイントを提供する必要があります。
  • シングルトンパターンは、ログ記録、ドライバーオブジェクト、キャッシング、およびスレッドプールに使用されます。
  • Singletonデザインパターンは、Abstract FactoryBuilderPrototypeFacadeなど、他のデザインパターンでも使用されています。
  • Singletonデザインパターンは、コアJavaクラスでも使用されています(たとえば、java.lang.Runtimejava.awt.Desktopなど)。

Java Singletonパターンの実装

Singletonパターンを実装するには、異なるアプローチがありますが、すべてのアプローチには次の共通の概念があります。

  • 他のクラスからクラスのインスタンス化を制限するためのプライベートコンストラクタ。
  • クラスの唯一のインスタンスである同じクラスのプライベートな静的変数。
  • クラスのインスタンスを取得するための外部からのグローバルアクセスポイントであるパブリックな静的メソッド。

さらなるセクションでは、シングルトンパターンの実装と設計上の懸念について学びます。

1. 熱心な初期化

熱心な初期化では、シングルトンクラスのインスタンスはクラスのロード時に作成されます。 熱心な初期化の欠点は、クライアントアプリケーションがそれを使用していなくても、メソッドが作成されることです。 静的初期化シングルトンクラスの実装は次のとおりです:

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // クライアントアプリケーションがコンストラクタを使用しないようにするためのプライベートコンストラクタ
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance() {
        return instance;
    }
}

シングルトンクラスが多くのリソースを使用していない場合、これが使用する方法です。 しかし、ほとんどのシナリオでは、シングルトンクラスはファイルシステム、データベース接続などのリソースのために作成されます。 クライアントが getInstance メソッドを呼び出すまで、インスタンス化を避けるべきです。 また、この方法では例外処理のオプションが提供されません。

2. 静的ブロックの初期化

静的ブロックの初期化実装は、イーガー初期化と似ていますが、クラスのインスタンスが例外処理のオプションを提供する静的ブロックで作成される点が異なります。

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    //例外処理のための静的ブロック初期化
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

    public static StaticBlockSingleton getInstance() {
        return instance;
    }
}

イーガー初期化と静的ブロック初期化は、使用される前にインスタンスが作成されるため、最良の方法ではありません。

3. 遅延初期化

シングルトンパターンを実装するための遅延初期化方法は、グローバルアクセスメソッドでインスタンスを作成します。このアプローチでシングルトンクラスを作成するサンプルコードは次のとおりです:

package com.journaldev.singleton;

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

    public static LazyInitializedSingleton getInstance() {
        if (instance == null) {
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

前述の実装は単一スレッド環境の場合には問題ありませんが、マルチスレッドシステムの場合、複数のスレッドが同時にif条件内にある場合に問題が発生する可能性があります。これはシングルトンパターンを破壊し、両方のスレッドが異なるインスタンスを取得する可能性があります。次のセクションでは、スレッドセーフなシングルトンクラスを作成するさまざまな方法を見ていきます。

4. スレッドセーフシングルトン

A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:

package com.journaldev.singleton;

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }

}

前述の実装は問題なく動作し、スレッドセーフを提供しますが、同期メソッドに関連するコストがかかるため、パフォーマンスが低下します。ただし、最初の数個のスレッドが個別のインスタンスを作成する可能性がある場合にのみ必要です。この余分なオーバーヘッドを毎回避けるために、ダブルチェックロック原則が使用されます。このアプローチでは、同期ブロックがif条件の内部で使用され、シングルトンクラスのインスタンスが1つだけ作成されることを確認するための追加のチェックが行われます。次のコードスニペットは、ダブルチェックロックの実装を提供します:

public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
    if (instance == null) {
        synchronized (ThreadSafeSingleton.class) {
            if (instance == null) {
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

学習を続けるには、スレッドセーフシングルトンクラスを参照してください。

5. ビル・ピュー・シングルトンの実装

Java 5より前、Javaメモリモデルには多くの問題があり、以前のアプローチはシングルトンクラスのインスタンスを同時に取得しようとするスレッドが多すぎる場合に失敗することがありました。そのため、Bill Pugh内部の静的ヘルパークラスを使用してシングルトンクラスを作成する異なるアプローチを考案しました。以下は、Bill Pughシングルトン実装の例です:

package com.journaldev.singleton;

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

シングルトンクラスのインスタンスを含むプライベートな内部静的クラスに注目してください。シングルトンクラスがロードされると、SingletonHelperクラスはメモリにロードされず、getInstance()メソッドが呼び出されたときにのみ、このクラスがロードされ、シングルトンクラスのインスタンスが作成されます。これは、同期が不要であるため、シングルトンクラスに最も広く使用されているアプローチです。

6. シングルトンパターンを破壊するためにリフレクションを使用する

リフレクションを使用して、以前のすべてのシングルトン実装アプローチを破壊することができます。以下は、例です:

package com.journaldev.singleton;

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                // このコードはシングルトンパターンを破壊します
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

前のテストクラスを実行すると、両方のインスタンスのhashCodeが異なることに気付くでしょう。これにより、シングルトンパターンが破壊されます。リフレクションは非常に強力であり、SpringやHibernateなどの多くのフレームワークで使用されています。Javaリフレクションチュートリアルで学習を続けてください。

7. Enum Singleton

この状況をリフレクションで克服するために、Joshua Blochはシングルトンデザインパターンの実装にenumの使用を提案しています。Javaはenumの値がJavaプログラム内で一度だけインスタンス化されることを保証しているため、Java Enumの値もシングルトンとしてグローバルにアクセスできます。欠点は、enum型がある程度柔軟性に欠けることです(たとえば、遅延初期化は許可されません)。

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // 何かを行う
    }
}

8. シリアル化とシングルトン

時々、分散システムでは、Serializable インターフェースをシングルトンクラスに実装する必要があり、その状態をファイルシステムに保存し、後で取得することができます。以下は、Serializable インターフェースも実装した小さなシングルトンクラスの例です:

package com.journaldev.singleton;

import java.io.Serializable;

public class SerializedSingleton implements Serializable {

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}

    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }

}

シリアライズされたシングルトンクラスの問題は、デシリアライズするたびにクラスの新しいインスタンスが作成されることです。以下はその例です:

package com.journaldev.singleton;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        // ファイルからオブジェクトにデシリアライズ
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());

    }

}

このコードは次の出力を生成します:

Output
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522

これにより、シングルトンパターンが破壊されます。このシナリオを克服するためには、readResolve() メソッドの実装を提供するだけです。

protected Object readResolve() {
    return getInstance();
}

その後、テストプログラムで両方のインスタンスのhashCodeが同じであることがわかります。

Java シリアライゼーションおよびJava デシリアライゼーションについて読んでみてください。

結論

この記事では、シングルトンデザインパターンについて説明しました。

さらにJava チュートリアルで学習を続けてください。

Source:
https://www.digitalocean.com/community/tutorials/java-singleton-design-pattern-best-practices-examples