Javaの単一インスタンスパターンの詳細解説

単一インスタンスパターンは、クラスがただ一つのインスタンスを持つことを保証するデザインパターンです。

単一インスタンスパターンでは、新しいインスタンスを作成するための公開コンストラクタを提供してはなりません。また、デフォルトの無引数コンストラクタもプライベートに変更する必要があります。そのため、単一インスタンスパターンのクラスは、既に作成されたインスタンスを返す静的メソッドを持たなければなりません。以下に、単一インスタンスパターンの基本的な構造を示します。

public class SingletonDemo {
    private static SingletonDemo instance = new SingletonDemo();

    private SingletonDemo() {}

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

あるいは、以下のようになります。

public class SingletonDemo {
    private static SingletonDemo instance;

    static {
        instance = new SingletonDemo();
    }

    private SingletonDemo() {}

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

これらの方法は、クラスがロードされるときにインスタンスが生成される「お腹いっぱい方式」(Eager Initialization)と呼ばれます。この方式は、初期化がリソースを消費する場合や、インスタンスが使用されない場合に無駄なリソースを使用する可能性があります。その問題を解決するためには、実際に必要になるまでインスタンスを生成しない「お腹空かせ方式」(Lazy Initialization)があります。

public class SingletonDemo {
    private static SingletonDemo instance;

    private SingletonDemo() {}

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

この方式は、インスタンスがまだ生成されていない場合に初めて生成します。しかし、この方法はマルチスレッド環境で問題が発生する可能性があります。複数のスレッドが同時に getInstance メソッドを呼び出した場合、2つのインスタンスが生成されてしまう可能性があります。これを防ぐために、synchronized キーワードを使用して同期制御を行います。

public class SingletonDemo {
    private static SingletonDemo instance;

    private SingletonDemo() {}

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

ただし、この方法はパフォーマンスに影響を与える可能性があります。一度しか実行されない初期化のために、すべての getInstance 呼び出しでロックを行うのは非効率的です。この問題を解決するためには、内部クラスを使用する方法があります。

public class SingletonDemo {
    private SingletonDemo() {}

    private static class SingletonHolder {
        private static final SingletonDemo INSTANCE = new SingletonDemo();
    }

    public static SingletonDemo getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

この方法は、SingletonDemo クラスがロードされるときに SingletonHolder クラスはロードされません。getInstance メソッドが初めて呼び出されたときにのみ SingletonHolder がロードされ、インスタンスが生成されます。これにより、遅延初期化とスレッドセーフ性を両立することができます。

ただし、反射やシリアライズ/デシリアライズを使用すると、異なるインスタンスが生成されてしまう可能性があります。この問題を解決するためには、readResolve メソッドをオーバーライドすることで、シリアライズ/デシリアライズ時のインスタンスを制御できます。

private Object readResolve() {
    return SingletonHolder.INSTANCE;
}

それでも、すべてのシリアライゼーション方式をカバーすることはできません。最も安全かつ簡潔な方法は、列挙型を使用することです。

public enum SingletonDemo {
    INSTANCE;
}

列挙型は、デフォルトで final であり、反射やシリアライズ/デシリアライズによって新たなインスタンスが生成されることはありません。以下にテストコードを示します。

public static void main(String[] args) throws Exception {
    SingletonDemo instance1 = SingletonDemo.INSTANCE;
    String str = JSON.toJSONString(instance1);
    SingletonDemo instance2 = JSON.parseObject(str, SingletonDemo.class);

    System.out.println(instance1.toString());
    System.out.println(instance2.toString());
    System.out.println(instance1 == instance2);
}

結果は以下の通りです。

INSTANCE
INSTANCE
true

列挙型を使用することで、単一インスタンスパターンの要件を満たしながら、最も簡潔で安全な実装が可能です。

タグ: Java 単一インスタンスパターン デザインパターン 列挙型 スレッドセーフ

7月17日 20:55 投稿