Singleton 変則double-checked locking

問題の理解に誤りがあったので追記しました。(2008-07-27)


Singletonパターンを用いるときに、次のような書き方をすることがある。

public class SingletonA {
    // ただ一つのインスタンスを保持するためのフィールド。
    private static SingletonA instance = null;

    // インスタンスを自由に生成させない。
    private SingletonA() {
    }

    public static SingletonA getInstance() {
        // ただ一つのインスタンスが生成されていない場合だけ生成に進む。
        if (instance == null) {
            // パフォーマンス低下を防ぐためにここでsynchronizedを使う。
            synchronized (SingletonA.class) {
                // 一つ目のifを複数のスレッドがすり抜ける場合があるので再チェック。
                if (instance == null) {
                    // 一番初めにアクセスしたスレッドでのみインスタンスを生成。
                    instance = new SingletonA();
                }
            }
        }

        // ただ一つのインスタンスを返す。
        return instance;
    }
}

これはdouble-checked lockingイディオムと呼ばれる手法だ。初めてこの手法を知ったときは、ifとsynchronizedの組み合わせ方が素晴らしく、なるほどなぁと思ったものだ。


しかし、実際にはこの方法はうまく動作しない。このことは先日知ったばかりなのだけど、アウトオブオーダー書き込みという現象が原因のようだ。

double-checked lockingの背景となっている理屈は申し分ありません。残念ながら、現実はまったく異なります。

(中略)

このメモリー・モデルは、いわゆる「アウトオブオーダー書き込み」を許し、それがこのイディオムの障害の主要な原因となっています。

http://www.ibm.com/developerworks/jp/java/library/j-dcl/

「double-checked lockingイディオム」と呼ばれており、完全な方法ではない。これはJavaのメモリモデルがアウトオブオーダー書き込みを許していることに起因している。

http://d.hatena.ne.jp/cxx03667/20071010/1191981355

※前者に詳細な説明が、後者に簡潔な説明と代替案がある。



さて、上記引用サイトの後者(http://d.hatena.ne.jp/cxx03667/20071010/1191981355)には様々な代替案が示されているが、どれも完全ではないようだ。そこで、double-checked lockingイディオムを改変した、変則double-checked lockingイディオムを考えてみた。


やっていることは単純で、booleanのフラグを使ってインスタンスが生成されたかをチェックしているだけだ。

public class SingletonB {
    // インスタンスが生成されたかどうかのフラグ。
    private static boolean generated = false;
    // ただ一つのインスタンスを保持するためのフィールド。
    private static SingletonB instance;

    // インスタンスを自由に生成させない。
    private SingletonB() {
    }

    public static SingletonB getInstance() {
        // ただ一つのインスタンスが生成されていない場合だけ生成に進む。
        if (!generated) {
            // パフォーマンス低下を防ぐためにここでsynchronizedを使う。
            synchronized (SingletonB.class) {
                // 一つ目のifを複数のスレッドがすり抜ける場合があるので再チェック。
                if (!generated) {
                    // 一番初めにアクセスしたスレッドでのみインスタンスを生成。
                    instance = new SingletonB();
                    // インスタンスが生成されたのでフラグをtrueにする。
                    generated = true;
                }
            }
        }

        // ただ一つのインスタンスを返す。
        return instance;
    }
}


このようにすれば、インスタンスのメモリ領域が確保された瞬間にそのinstanceフィールドが非nullになっても、generatedフィールドはfalseのままなので大丈夫というわけだ。


JDK1.5以上であっても、Singletonパターンを使う場合はdouble-checked lockingを使わない方法を考えたほうがいいみたい。

http://d.hatena.ne.jp/skirnir/20080705/1215241339

などとも述べられているdouble-checked lockingイディオムだが、このような改変を加えることで使えないだろうか。

追記

問題の理解を誤っていた。

// 一番初めにアクセスしたスレッドでのみインスタンスを生成。
instance = new SingletonB();
// インスタンスが生成されたのでフラグをtrueにする。
generated = true;

の部分の1行目よりも先に2行目が実行されてしまうこともあるようだ。これではSingletonBはうまく機能しない。


しかし、この部分を次のように書き換えるとどうなるのだろう?最適化が行われるとは言え、これならインスタンス生成が行われてからしかgeneratedをtrueにできないように思える。

public class SingletonC {
    private static boolean generated = false;
    private static SingletonC instance;

    private SingletonC() {
    }

    // trueを返すメソッド。
    private boolean getTrue() {
        return true;
    }

    public static SingletonC getInstance() {
        if (!generated) {
            synchronized (SingletonC.class) {
                if (!generated) {
                    instance = new SingletonC();
                    // インスタンスが生成されてからしかgetTrueは実行できないのでは?
                    generated = instance.getTrue();
                }
            }
        }

        return instance;
    }
}

私の知識ではこれがうまくいくのかわからないのだが、このようにしても実行順序が入れ替わるようなことが起こり得るのだろうか?