Featured image of post デザインパターン、Singleton

デザインパターン、Singleton

昔からPCを使うといつも問題となるのはメモリーだった覚えがあります。私が初めてPCに触れたのは父が仕事て使っていたもので、当時はOSとしてDOSを採用していてゲームでもしたいときはいつもメモリーの設定を変える必要がありました。そのときはそれが不便だとも思わず、ただゲームができればいいと思っていました。

しかし時間が経ち、大学でのプレゼンテーションを準備しながら感じたのは、やはりメモリーが十分でないとマルチタスクがきついということでした。今はPCのパーツの中でもっともアップグレードした時に性能向上を感じられるのはSSDと言いますが、それはあくまでCPUとメモリーを安定的に確保できる時代になったおかげと思います。まずメモリーが足りないととにかく遅いとしか思えない時代もありましたからね。

そしてプログラムを作る立場となってからは、メモリー問題はより現実的な問題となりました。例えばとあるシステムを構築し、複数の使用者がそのシステムを利用するとしたら、限定された資源であるメモリーが足りなくなる可能性はハードウェアが飛躍的な発展を成している現在でも存在しています。オブジェクトを作るたび、残りのメモリーは減り続けるので。

ならば最適化という面で、メモリーを節約するには、無駄なオブジェクトの生成は抑えるべきでしょう。そのためにできる方法がないかと思っていたら、すでに存在していました。今回のポストの主題となる、Singletonパターンです。

Singletonパターンとは

Singletonパターンは、アプリケーション内でインスタンスが一度だけ生成され、そのアプリケーションが終了するまで使われるクラスを作るためのデザインパターンです。Beanの場合はそれぞれ違うデータを持つインスタンスをたくさん生成して使いますが、こちらはインスタンスが一つしかないため動的なフィールドを持たせないですね。なのでどこからでもアクセスできるような不変のデータを持ったり、特定の処理を繰り返す必要がある時このSingletonパターンでのクラスを作る場合があります。

このようなクラスがあって何が良いかというと、先に述べたメモリー問題です。例えばグローバル変数の場合、どんなクラスからもアクセスできるのであまりSingletonと変わらないようにも見えます。しかしグローバル変数の場合は、それが使われるが使われないが常にメモリーのなかにあって、無駄になってしまう可能性もあります。しかしSingletonの場合は、必要であれば生成し、必要でなければ生成しないこともできます。なのでメモリーを節約できますね。

仕事では主に、ユーティリティークラスとしてSingletonクラスを作ることが多かったです。データの処理を繰り返して行う必要がある時、毎回インスタンスを生成することはメモリー問題もあって、コードが無駄に冗長になる傾向がありました。これをデータはそれぞれ違うインスタンスのBeanに持たせ、Singletonクラスに処理を任せることでコードの量も減らし、メモリーも節約することができました。

古典的Singletonパターン

それではSingletonクラスをどうやって作るのかを紹介します。デザインパターンでは様々なパターンがあって、その中の一つであるSingletonもまた様々な方法で具現できます。まずは古典的な方式を紹介します。

ここで目的はインスタンスを一つだけにすることなので、外部からすでに生成されているインスタンスにアクセスはできても、そのインスタンスを勝手に作れないようにします。そうするにはコンストラクターにアクセスの制限が必要ですね。まずコードで紹介しましょう。

// クラスはpublicにして外部からアクセスできるようにする
public class SingletonClass {

    // コンストラクターはprivateにして、外部からはアクセスできないようにする
    private SingletonClass() {}
}

しかしこれだけでは十分ではありません。どこかでインスタンスを生成する必要がありますね。また、先に述べたようにインスタンスの生成の時点は外部で制御できるようにしなければなりません。なのでprivateのコンストラクターにアクセスできるメソッドを用意する必要があります。

public class SingletonClass {

    // インスタンスを保存するための静的フィールド
    private static SingletonClass uniqueInstance;

    private SingletonClass() {}

    // インスタンスの返却(インスタンスが生成されたない場合は生成してから返却する)
    public static SingletonClass getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new SingletoneClass();
        }
        return uniqueInstance;
    }

    public void doSomething() {
        // ... 普通のメソッド
    }
}

まずstaticで、自分のインスタンスを保存できるフィールドを宣言します。外部からSingletonクラスのインスタンスを取得するためにはこれを使うようになります。宣言だけで、この段階でインスタンスを生成しないのはグローバル変数と区別するためです。

次に、インスタンスが生成されてない場合でもアクセスできるstaticメソッドを作成します。ここからこのSingletonクラスのインスタンスを取得するようになります。メソッドの中はでは戻り値としてインスタンスのフィールドをセットし、もしインスタンスが生成されてない場合にだけnewをするようにします。

これで外部からは以下のように使えるようになります。

// インスタンスの取得
SingletonClass singletonInstance = SingletonClass.getInstance();

// インスタンスのメソッドを使用
singletonInstance.doSomething();

これでどこからでも同一なインスタンスでつかけるSingletonクラスができました。

古典的Singletonパターンの問題

マルチスレッドを考える必要がない場合なら気にすることはないですが、現代のプログラミングはそうでもないですね。特に何かのシステムを作り、サービスとして提供する場合は複数の使用者によって同じクラスが要請される場合があります。

そしてクラスの中が複雑でインスタンスの生成に時間がかかったり、ほぼ同時のタイミングでインスタンスが要請されると古典的なSingletonパターンでは複数のインスタンスが生成されることを塞げられない場合があります。この場合は元の設計通り動かなくなり予想できない例外が発生する可能性がありますね。

もちろんこれらを解決するためにいくつかの方法が提示されてはいますが、それらの解決策にもデメリットはあります。まずどんな方法があり、それぞれのデメリットには何があるか見ていきましょう。

マルチスレッド問題を開所するために

他にも方法はありそうですが、スレッドセーフなSingletonクラスを生成する方法は以下のようなものがあります。

  1. インスタンスの生成をシンクロさせる
  2. Double-Checked Lockingを使う
  3. JVMのクラスローダーにお任せ

まずインスタンスの生成をシンクロさせる方法は簡単です。インスタンスを取得するためのgetInstance()メソッドにsynchronizedを追加することです。コードで見るとあまり変わらないので、古典的Singletonパターンのクラスがあればもっとも簡単に適用できる方法ですね。

public class SingletonClass {

    private static SingletonClass uniqueInstance;

    private SingletonClass() {}

    // インスタンスを提供するメソッドをシンクロさせる
    public static synchronized SingletonClass getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new SingletoneClass();
        }
        return uniqueInstance;
    }
}

ただ、synchronizedの問題は性能です。100倍以上も処理の速度が遅くなる場合もあるらしいので、マルチスレッドを性能のために使う場合があれば、あまり望ましくないですね。

次の方法は、二重チェックです。インスタンスがnullであればシンクロさせます。この方法だと毎回シンクロさせる必要がないので(2回目からはインスタンスがnullでないため)最初の一回以外は性能が低下しません。

public class SingletonClass {

    // volatile宣言で安定性を確保
    private volatile static SingletonClass uniqueInstance;

    private SingletonClass() {}

    // インスタンスの二回確認
    public static SingletonClass getInstance() {
        if (uniqueInstance == null) {
            synchronized (SingletonClass.class){
                if (uniqueInstance == null) {
                    uniqueInstance = new SingletoneClass();
                }
            }
        }
        return uniqueInstance;
    }
}

volatile宣言を使う理由は、変数がCPUのキャッシュメモリーに入ることを防止するためだそうです。プログラムのデータは最初ハードディスクから読み込まれシステムメモリーに載せられますが、そのあとCPUでの処理が行われる時にはさらにCPUのキャッシュメモリーに載せられることがあります。

最近は複数のCPUを搭載しているシステムも少なくないので、それぞれ違うCPUのキャッシュメモリーにインスタンスが入ってしまうとインスタンスが生成されているかどうかわからなくなりますね。volatile宣言でシステムメモリーにフィールドを乗せることで、インスタンスの生成がより安定的に行われます。でも依然として、同期化による性能低下を一回は経験しなければならないという問題があります。

最後は、JVMが起動する時にインスタンスを生成させる方法です。この方法では外部からは確実にインスタンスの制御ができなく、常にインスタンスが生成されるためマルチスレッド問題を回避できますね。

public class SingletonClass {

    // フィールドにインスタンスの生成を宣言
    private static SingletonClass uniqueInstance = new SingletonClass();

    private SingletonClass() {}

    // インスタンスのチェックも要らなくなる
    public static SingletonClass getInstance() {
        return uniqueInstance;
    }
}

クラスが読み込まれる時点でJVMからインスタンスを生成してしまうので、どんなスレッドからも静的フィールドにはアクセスできなくなります。ただ、これならグローバル変数で宣言することとあまり変わらないので、使われなくてもメモリー上にはインスタンスが生成されたままであるという問題は残ります。もちろん、グローバル変数宣言とは違ってインスタンスは唯一であることが違うところです。グローバル変数だとstaticでフィールドを宣言しても、違うクラスでまたを宣言できますので。そもそもグローバル変数には何が入っているかわからなくなる場合が少なくないので、乱発しないほうがいいですね。

メソッドとフィールドを全部staticに宣言して良いのでは?

もちろんその方法もありです。しかし、初期化の過程が極めて簡単な場合にだけ有効(フィールドが何もないなど)な方法と言えます。クラス自体が単純な構造をしていて、メソッドは単純に外部から入れられたデータを処理して返すだけならできる方法ですね。実際使えない方法ではないですが、後の機能拡張などを考えると良い方法ではなくなりますね。

最後に

Singletonパターンは幅広く使われていて、確かに魅力的なクラスの設計の方法ではあります。しかしマルチスレッド問題を回避するため工夫しなければならない問題があり、唯一なインスタンスなためフィールドの処理にも気をつけなければならない面があります。とあるスレッドでインスタンスが使われていて、フィールドにデータを入れたのをまた違うスレッドでアクセスしようとすると問題が起こり得る可能性がありますので。

他にもOOPの原則である、「一つのクラスは一つだけの責任を持つ」ということからしても、Singletonクラスは問題を持っています。何かの処理を担当していながらも、自分自身でインスタンスを管理するという二つの責任を持っていますからね。そしてコンストラクターがprivateであるため、サブクラスを作られなくなるという問題があります。サブクラスを生成するためにコンストラクターをpublicやprotectedに変えるとSingletonではなくなるジレンマもできますね。

それでもSingletonパターンで作られたクラスは、確かな魅力を持っています。ちゃんとインスタンスの管理さえできていれば、どこでも呼びたして使うことができますからね。常にメモリーに載せる必要があるクラスができたら、検討したくなるパターンです。

Built with Hugo
Theme Stack designed by Jimmy