🤖

【Java】スレッドセーフなシングルトン

に公開

シングルトン

シングルトンとは、デザインパターンの1種であり、クラスのインスタンスが常に1つしか生成されないことを保証するもの。これによりシステム全体で共有されるべき単一のリソース(設定ファイル、ログ出力、データ接続など)を管理するのに役立ちます。

仕組みとしては、コンストラクタを非公開にして、静的メソッドで唯一のインスタンスを取得するようにすることで(インスタンス生成を制御する)シングルトンパターンとなります。
下記のコードは、遅延初期化することでシングルトンを実現している実装です。

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {}; // 外部からのインスタンス生成を禁止

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

ただ、このコードには欠点があります。
後ほど、詳しく解説します。

シングルトンが必要なユースケース

1. ログ出力クラス

アプリケーション全体で1つのロガーインスタンスを共有することで、ログファイルへのアクセスが競合するのを防ぎます。

もし、シングルトンがない場合、ログ出力を行う各モジュール(クラス)でロガーインスタンスを生成(new Logger())することになります。この場合、ログファイルへのアクセス競合状態が発生して、ログが正しく書き込まれない、ファイルが破損したりする可能性があります。
また、単純にロガーインスタンスを各モジュールで生成したらリソースの無駄遣いになります。GCの実行頻度も上がり、STWが発生、結果としてレイテンシーの低下に繋がります。

2. スレッドプールやコネクションプール

スレッドやデータベースのコネクションの生成・管理を行うクラスは、システム全体でひとつに集約することでリソースを効率的に管理できます。
シングルトンを使わない場合、各コンポーネントがそれぞれ独自のプールインスタンスを生成することになります。これにより、

・管理の複雑化
・パフォーマンスの低下

などに繋がります。

このようにシングルトンはグローバルなアクセスポイントを提供しつつ、リソースの競合や整合性を防ぐ目的で使用されます。

マルチスレッドプログラミングにおけるシングルトン

Javaなどマルチスレッドプログラミングでは、スレッドセーフなシングルトンが必要です。

そもそもスレッドセーフってなに?

まず、Javaはマルチスレッドで実行するプログラミング言語になります。
マルチスレッドということはヒープメモリは共有リソースとなります。
そのため、共有リソースに複数スレッドがアクセスしても、問題ないように(データに不整合が生じないように)設計する必要があります。

ちなみにスレッドセーフとは、特定のコードに対してマルチスレッドで並行的に実行しても問題ないことを意味します。

遅延初期化のアプローチはスレッドセーフなシングルトンとはいわない

「シングルトンとは?」の章で提示した遅延初期化のアプローチは「スレッドセーフなシングルトン」とはいいません。
例えば、スレッドAがif(uniqueInstance == null)を通過してインスタンスを生成する前に、スレッドBに処理が切り替わり、スレッドBもまだインスタンス生成がされていないと判断し、if(uniqueInstance == null)を通過します。
結果として複数のインスタンスが生成されてしまい、シングルトンの原則は破綻します。

スレッドセーフなシングルトン実装方法

では、スレッドセーフなシングルトンにするためにはどうすれば良いのでしょうか?
下記より、実装パターンをいくつか紹介していきます。

静的初期化

このパターンは、クラスのロード時にインスタンスを生成します。
最もシンプルで安全な方法です。

public class Singleton {
    private static final Singleton uniqueInstance = new Singleton();

    private Singleton() {}; 

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

遅延初期化(synchronized)

このパターンは、冒頭に挙げたコード例にsynchronizedキーワードを付け加えたものです。
synchronizedキーワードは、Javaのマルチスレッド環境において、クリティカルセッション(複数のスレッドから同時にアクセスされると問題が発生する可能性があるコード)を保護する役割を果たします。これによって、競合状態を回避して、スレッドセーフなシングルトンとなります。
この方法はシンプルですが、インスタンスが一度生成された後も毎回ロック処理が発生するため、パフォーマンスがクリティカルな場面ではオーバーヘッドとなる可能性があります。

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {};

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

Double-Checked Locking(DCL)

遅延初期化(synchronized)パターンのオーバーヘッドを解消するには、DCLパターンが有効です。
DCLは、以下のように二重でnullチェックすることで、インスタンスが既に存在する場合はロックを回避して、必要な時だけ同期を行うようにします。これにより不要なロックを減らします。

public class Singleton {
    private static volatile Singleton uniqueInstance;

    private Singleton() {};

    public static Singleton getInstance() {
        if(uniqueInstance == null) {
            synchronized (Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

volatileキーワードは、マルチスレッド環境における命令の並び替えを防ぐために不可欠です。
これにより、インスタンス初期化が完了する前に他のスレッドから見えてしまう問題を回避できます。

Enum Singleton

Enumは、Java言語の仕様により、インスタンスがひとつしか生成されないことが保証されています。この特性を利用してシングルトンを実装する方法がEnum Singletonです。

public enum Singleton {
    INSTANCE;
    public void doSomething() {
    }
}

// 呼び出し側
public class Main {
    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.doSomething();
    }
}

Discussion