かとじゅんの技術日誌

技術の話をするところ

遅延初期化には気をつけろ

フィールドの遅延初期化について勉強したので要約としてまとめておきます。

遅延初期化とは

遅延初期化とは、コンストラクタなどで事前にフィールドを初期化するのではなく、フィールドが利用される時に初期化することいいます。遅延初期化は諸刃の剣と呼ばれていて、必要がなければやらないというのが考え方らしい。

public class Fuga{
    private Hoge hoge;
    public Hoge getHoge(){
        if ( hoge == null ){
            hoge = new Hoge(); // 遅延初期化している
        }
        return hoge;
    }
}

ただ、この場合だと複数のスレッドから呼び出されてしまうと、遅延初期化が循環してしまいます。(遅延初期化循環といいます)ではなく、複数のスレッドが割り込むと初期化が何度も行われてしまうということ。
追記:さらにいうとシングルトンは単一のインスタンスを返すはずが、これによって複数のインスタンスができてしまう問題があります。
さらに追記:複数のインスタンスを戻り値で返す可能性があります。シングルトンは、アプリケーション内で、常にインスタンスはひとつという不変条件を壊していることになります。
対策としては、以下のようにgetterをsynchronizedで同期化します。これが典型的な遅延初期化の方法です。

public class Fuge{
    private Hoge hoge;
    public synchronized Hoge getHoge(){
        if ( hoge == null ){
            hoge = new Hoge(); // 遅延初期化している
        }
        return hoge;
    }
}

クラスフィールドを使う場合の遅延初期化

シングルトンパターンのようにクラスフィールドを遅延初期化する場合です。前例と同様にsynchronizedで同期化したパターンで書けます。このコードは頻繁に見かけるのではないでしょうか。

public class Hoge{
    private static Hoge instance;
    public synchronized static Hoge getInstace(){
        if ( hoge == null ){
            hoge = new Hoge(); // 遅延初期化している
        }
        return hoge;
    }
}

もう少し変則的というか、目から鱗なパターンがあります。こちら。

public class Hoge{
    private static class InstanceHolder{
        static final Hoge instance = new Hoge();
    }
    public static Hoge getInstace(){
        return InstanceHolder.instance;
    }
    private Hoge(){
    }
}

インスタンスがインナークラス内部のクラスフィールドになっていて、getInstnaceメソッドにはsynchronizedで同期化されていません。
実は、getInstaceメソッドが初めてコールされた時に初めてInstanceHolderクラスが初期化されます。それによってInstanceHolderのinstannceが初期化されます。また、同時にクラスの同期化時にJVMがフィールドを同期化するため、前例と同様のことが可能になります。InstanceHolderクラスが初期化された後は同様のプロセスが実行されないようにJVMがコードを修正します。だから、同期化の必要がないわけです。
個人的には同期化もそうなんですが、if文を書かない方が精神的な負荷が少ないので、こっちのほうがいいなーと思っているところですw

パフォーマンス改善のための二重チェックによる遅延初期化

また、最初に紹介したインスタンスフィールドの遅延初期化のパフォーマンスを改善したい場合は二重チェックの方法がよいです。

public class Fuge{
    private volatile Hoge hoge;
    public Hoge getHoge(){
        Hoge result = hoge;
        if ( result == null ){ // ロックしないで検査
            synchronized(this){
                result = hoge; // フィールドが既に初期化されている場合に必要
                if ( result == null ){ // ロックして検査
                    result = hoge = new Hoge();
                }
            }    
        }
        return result;
    }
}

1回目のnullチェックはロックコストを最小にするために行います。これがないと無条件にsynchronizedが呼ばれてパフォーマンスが落ちます。ロックを取得するまでに既にhogeが初期化されている場合もあるため、ロック取得直後から再度result = hogeを行い改めて2回目のnullチェックを行います。その際にnullの場合はインスタンスを初期化します。個人的にはパフォーマンスの改善が求められるときに、このような仕組みを適用するとよいと思っています。

遅延初期化するとフィールドのアクセスコストは増大するわけですが、その犠牲を払ってでもクラスやフィールドの生成コストを下げたい場合に有用ということです。いずれにしても、使い方には気をつけよう。

追記:

    private volatile Hoge hoge;

しているので

public Hoge getHoge(){
        Hoge result = hoge;
        if ( result == null ){ // ロックしないで検査

は、スレッドが並行でhogeを読み書きしている状況でも、アトミックにhogeを取得できるはず。だから、if ( result == null )が常に不成立になることはないと思います。

追記:
コメントにもいただいたのですが、JDK1.5より前では上記の二重チェックは機能しないようです。volatileのセマンティクスが十分に機能しないとか。JDK1.5以降はこの問題が解消されているそうです。ご指摘のとおり、内部クラスのパターンが一番よいですね。

あわせて読みたい
http://d.hatena.ne.jp/asakichy/20091125/1259103136