LoginSignup
2
2

デザインパターン Singletonパターン

Last updated at Posted at 2023-10-05

Singletonパターン

インスタンスが1つしか存在しない唯一無二のオブジェクト。特定の条件下でインスタンス化が行われ、その後は再利用される。

クラス内にプライベートなコンストラクタを持ち、インスタンスを取得するための静的(static)メソッドを提供する。

アプリケーションの設定や、構造上複数存在してはいけない概念を扱う際に利用する。

基本型

Java
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

        return instance;
    }
}

最適形

Javaのenum型の特性を利用してシングルトンを実現することができる。

Java
public enum Singleton {
    UNIQUE_INSTANCE;
}

Javaの列挙型の列挙定数は、その列挙定数自体がインスタンス。列挙型が初めて参照されるときに列挙定数が初期化され、その後は再利用される。

さらに、インスタンスが生成される際にスレッドセーフ(複数のスレッドから同時に実行されても競合しない)であることが保証されている。

また列挙型は、デフォルトでシリアライズとデシリアライズがサポートされているため、インスタンスを丸ごと保存したり、再現したりすることができる。

さらに、コンパイラが特別な扱いを行うため、リフレクションによる攻撃(インスタンス生成)を防ぐことができる。リフレクションとは、プログラムの実行中にそのプログラム自体を分析、操作すること。例えば、リフレクションを利用することで、クラス内で定義されたフィールドやメソッドを一覧で取得したり、アクセス修飾子を無視してクラス内のフィールドへアクセスしたりできる。

スレッドセーフなSingleton

基本型では、複数のスレッドから同時にgetInstance()が実行された場合、インスタンス生成が競合を起こすことがある。

これを回避するにはいくつかの手法がある。

宣言時のインスタンス化

宣言時にインスタンス化をしてしまう方法。

Java
public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {}

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

二重チェックロッキング(Double-Check Locking:DLC)

遅延初期化を行いつつ、スレッドセーフを実現する手法。

Java
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        // このifは、複数のスレッド同時に実行できる
        if (instance == null) {
            synchronized (Singleton.class) {
                // このifは、常に一つのスレッドしか実行できない(パフォーマンスは悪い)
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile

volatileは「揮発性」という意味で、値が変化しやすいことをコンパイラに通知する役割を持つ修飾子。

volatileを付与したinstanceフィールドへのアクセスは、常にメインメモリ(主記憶)から行われることが保証される。

volatileを付与しないフィールドは、コンパイラやプロセッサによる最適化の対象となり、変数に格納された値は、メインメモリよりも高速にアクセスが可能なキャッシュメモリ上に保持され、再利用されることがある(ただし、この挙動はコンパイラに依存しているため、すべてのコンパイラが同じような挙動をするわけではない)。つまり、変数に代入を行う前にキャッシュメモリ上に変数の値がコピーされてしまうと、その後、メインメモリ上の変数に別の値を代入しても、代入前のキャッシュメモリ上の変数へのアクセスが行われてしまう。

volatile修飾子を使用したフィールドは、複数のスレッド間で共有され、常に最新の値にアクセスすることができるようになる。

通常Javaでは、変数の値はスレッドが利用するキャッシュメモリ(CPUとメインメモリとの間に配置される記憶領域)に保存されることがあり、複数のスレッドが同じ変数にアクセスしても、それぞれが異なる値を持つことがある。

volatileで宣言した変数は、常にメインメモリから読み出され、キャッシュされないため他のスレッドからの変更も即座に反映される。

synchronized

マルチスレッド環境において、排他制御を行うためのキーワード。

メソッドに対して使用すると、そのメソッドは複数のスレッドから実行されるときに、常に一つのスレッドしか実行できなくなる。

Java
public synchronized void synchronizedMethod() {
    // 常に一つのスレッドしか実行できない
}

ブロックに対して使用すると、そのブロックの処理は常に一つのスレッドしか実行できなくなる。

ブロックに対して使用する場合は、オブジェクトを指定する必要がある。スレッドはまず、指定されたオブジェクトへのロック(Lock)を獲得する。ロックが解放されない限り、別のスレッドはそのブロックの処理を実行することはできず、ロックが解放されるまで待機することになる。ロックを獲得しているスレッドの処理がブロックから抜けると、ロックは解放される。

そのため、指定するオブジェクトは実質、何でもよく、どのオブジェクトを選定するかはさほど重要ではない。

Java
Object lock = new Object();

synchronized (lock) {
    // 常に一つのスレッドしか実行できない
}

public staticなメンバとの違い

  • インスタンスの管理
    Singleton:初めてアクセスされたタイミングでインスタンスが生成され、その後、そのたった一つのインスタンスが何度も繰り返し再利用される。
    public static:再代入されることで、複数のインスタンスが生成される可能性がある。

  • 名前空間
    Singleton:クラス内のプライベート変数にインスタンスを格納するため、そのクラス外で使用する変数との名前の競合が起きにくい。
    public static:グローバル変数として宣言した変数にインスタンスを格納することになるため、そのクラスの外側で使用する変数との名前の競合が起きやすい。

  • メモリ
    Singleton:インスタンスが必要なタイミングで初めてメモリ上にロードされる。
    public static:一度クラスがメモリ上にロードされると、常に存在し続けることになるため、メモリの使用量が増える可能性がある。

  • 継承・インターフェースの実装
    Singleton:できる。
    public static:できない。

  • テスト
    Singleton:モック、スタブといったテスト用のオブジェクトと置き換えることが比較的容易。
    public static:置き換えることが難しい。

クラスローダとSingleton

深い理解ができなかったトピックです。理解が進んだ折に、追記したいと思います。(2023/10/1)

Javaには、複数のクラスローダが存在する。

異なる2つのクラスローダが、Singletonで設計されたあるクラスを、それぞれでロードしてしまった場合、2つのクラスは別のクラスとして扱われ、それぞれが独立してSingletonインスタンスを持つことになる。

(限られた条件下でのみ起こる現象だが、クラスローダについても理解が深まる良い機会なので、学習しておく。)

特に、「カスタムクラスローダとSingletonパターンは同時に使用する際に注意が必要」と覚えておく。

クラスローダ(Class Loader)とは

クラスローダは、Java仮想マシン(JVM)がクラスファイル(.classファイル)を動的にロードする役割を担うコンポーネント。Javaプログラムが実行される際に、特定のクラスが必要になると、対応するクラスファイルを探して、メモリにロードする。

そもそもJavaは、JIT(Just-in-Time)コンパイラを用いることで、実行前に既にコンパイルされたバイトコード(中間コード)を、実行時にさらに機械語にコンパイルして、プログラムを実行している。

プログラムが実行されるまでの流れ(Java)

1. コンパイル(Compile)
ソースコード(.java)がjavacコンパイラによってバイトコード(.class)にコンパイルされる

2. クラスローディングとJITコンパイル
コンパイルされたバイトコードが、クラスローダによってJVMにロードされ、JITコンパイラがさらに機械語へコンパイルする

3. 実行(Execute)
JITコンパイラによって機械語に変換されたバイトコードが実行される

クラスローディングの流れ

上記「2.クラスローディングとコンパイル」では以下が行われている。
2-1. クラスのロード(Loading)
クラスローダは、クラスファイル(.class)を読み込み、バイトコード(2進数で表現されたもの)として取得する

2-2. バイトコードの検証(Verification)
バイトコードが正しい構造であるかが検証される

2-3. バイトコードの準備(Preparation)
静的変数や静的メソッドを初期化する

2-4. クラスの初期化(Initialization)
クラスが初めて参照された際に、静的変数の初期化や静的初期化ブロックの実行を行う

Javaのクラスローダは大きく以下のように分類される。

起動クラスローダ(Bootstrap Class Loader)

Java仮想マシン(JVM)自体に組み込まれた、最上位のクラスローダ。

JRE(Java Runtime Environment)の核となる標準ライブラリや基盤的なクラスをロードする。実行環境によって提供されるクラスローダのため、Javaコードから直接アクセスしたり、カスタマイズできない。

拡張クラスローダ(Extension Class Loader)

起動クラスローダの子。

JREの<JAVA_HOIME>/lib/ext ディレクトリやjava.ext.dirs システムプロパティで指定されたディレクトリから、クラスをロードする。主に開発者が拡張機能を提供する際に使用する。

アプリケーションクラスローダ(Application Class Loader)

拡張クラスローダの子。

Javaアプリケーションのクラスをロードするクラスローダ。クラスパスに指定されたディレクトリや、JARファイルからクラスをロードする。

アプリケーションの実行時に利用される

カスタムクラスローダ

アプリケーション独自のクラスローダを実装することも可能で、特定の要件に合わせてクラスのロード方法をカスタマイズできる。

親子関係

クラスローダは、クラスを重複してロードするのを避けるために、親子関係を持っている。

基本的には、親が先にロードを行い、子は、親が既にロードしたクラスが存在する場合、そのロード済みクラスを使用する。このことによって、クラスの階層構造が維持され、クラスの重複ロードや競合を防ぐことができる。

ただし、クラスローダを自前で用意した場合(カスタムクラスローダ)などは、自前のクラスローダと既存のクラスローダが一つのクラスを重複してロードすることがある。

そのため、カスタムクラスローダと、シングルトンパターンを併用する場合には、インスタンスが複数生成される可能性があるということを理解しておく。

参考

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2