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

以下は、日本語で自然な言い方で表現したものです(一つのオプション):
イントロダクション

Java Singletonパターンは、Gang of Fourのデザインパターンの一つであり、Creational Design Patternのカテゴリーに属しています。定義からは、シンプルなデザインパターンのように思えますが、実装には多くの懸念が伴います。

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

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

  • Singleton pattern restricts the instantiation of a class and ensures that only one instance of the class exists in the Java Virtual Machine.
  • The singleton class must provide a global access point to get the instance of the class.
  • Singleton pattern is used for logging, drivers objects, caching, and thread pool.
  • Singleton design pattern is also used in other design patterns like Abstract Factory, Builder, Prototype, Facade, etc.
  • Singleton design pattern is used in core Java classes also (for example, java.lang.Runtime, java.awt.Desktop).

JavaのSingletonパターンの実装

シングルトンパターンを実装するためには、さまざまなアプローチがありますが、それらすべてには以下の共通の概念があります。

  • Private constructor to restrict instantiation of the class from other classes.
  • Private static variable of the same class that is the only instance of the class.
  • Public static method that returns the instance of the class, this is the global access point for the outer world to get the instance of the singleton class.

以下のセクションでは、シングルトンパターンの実装方法や実装に関するデザイン上の留意事項を学びます。

1. 熱意を持って初期化

イーガー初期化では、シングルトンクラスのインスタンスがクラスの読み込み時に作成されます。イーガー初期化の欠点は、クライアントアプリケーションが使用していなくてもメソッドが作成されることです。以下に、静的初期化シングルトンクラスの実装があります。

package com.scdev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // private constructor to avoid client applications using the constructor
    private EagerInitializedSingleton(){}

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

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

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

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

package com.scdev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // static block initialization for exception handling
    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.scdev.singleton;

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

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

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

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

スレッドセーフなシングルトンクラスを作成する簡単な方法は、グローバルアクセスメソッドを同期化することで、一度に1つのスレッドだけがこのメソッドを実行できるようにする方法です。以下にこのアプローチの一般的な実装例を示します。

package com.scdev.singleton;

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

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

}

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

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

スレッドセーフなシングルトンクラスを使って学習を続けましょう。

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

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

package com.scdev.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.scdev.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) {
                // This code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

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

7. 列挙型シングルトン

Reflectionを使ってこの状況を克服するために、ジョシュア・ブロッホはJavaがJavaプログラム内で任意のenum値が一度だけインスタンス化されることを保証しているため、シングルトンデザインパターンの実装にenumの使用を提案しています。JavaのEnum値はグローバルにアクセス可能であるため、シングルトンも同様です。デメリットとしては、enum型は柔軟性にやや欠けることです(例えば、遅延初期化は許可されていません)。

package com.scdev.singleton;

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // do something
    }
}

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

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

package com.scdev.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.scdev.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();

        // deserialize from file to object
        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のチュートリアルで学習を続けてください。

コメントを残す 0

Your email address will not be published. Required fields are marked *