Javaはどのようにして単例モードを正しく書くか

14464 ワード

単例モードは設計モードの中で最も理解しやすく、手書きコードが最も容易なモードでもあるでしょう.しかし、中には穴が少なくないので、面接問題としてもよく使われています.本文は主にいくつかの単例の書き方の整理に対して、そしてその優劣を分析します.多くはよくある問題ですが、スレッドの安全な単一例を作成する方法が分からない場合は、デュアルロックとは何か分からない場合は、この文章が役に立つかもしれません.
怠け者式、スレッドは安全ではありません
単一のパターンを実現すると聞かれると、多くの人の最初の反応は、教科書でもこのように教えてくれたコードを書くことです.
public class Singleton {
    private static Singleton instance;
    private Singleton (){}

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

このコードは簡単明瞭で、怠け者のロードモードを使用していますが、致命的な問題があります.複数のスレッドがgetInstance()を並列に呼び出すと、複数のインスタンスが作成されます.つまりマルチスレッドでは正常に動作しない.
怠け者式、スレッドセキュリティ
上記の問題を解決するために、最も簡単な方法はgetInstance()メソッド全体を同期(synchronized)に設定することです.
public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

スレッドのセキュリティが向上し、マルチインスタンスの問題が解決されましたが、効率的ではありません.getInstance()メソッドを呼び出すスレッドはいつでも1つしかないからです.ただし、同期操作は、1回目の呼び出し時にのみ必要です.すなわち、1回目のインスタンスオブジェクトを作成する場合です.これにより、二重検査ロックが引き出されます.
ダブルチェツクロック
二重検査ロックモード(double checked locking pattern)は、同期ブロックを用いてロックする方法である.プログラマーは二重検査ロックと呼んでいます.2回検査があるからです.  instance == nullは、一度は同期ブロックの外であり、一度は同期ブロック内である.なぜ同期ブロック内でもう一度検証するのですか?複数のスレッドが同期ブロック外のifに一緒に入る可能性があるため,同期ブロック内で二次検査を行わないと複数のインスタンスが生成される.
public static Singleton getSingleton() {
    if (instance == null) {                         //Single Checked
        synchronized (Singleton.class) {
            if (instance == null) {                 //Double Checked
                instance = new Singleton();
            }
        }
    }
    return instance ;
}

このコードは完璧に見えますが、残念ながら問題があります.主にinstance = new Singleton()という文にありますが、これは原子操作ではありません.実際にJVMでは次の3つのことをしています.
  • instanceにメモリ
  • を割り当てる
  • Singletonのコンストラクタを呼び出してメンバー変数
  • を初期化する.
  • instanceオブジェクトを割り当てられたメモリ領域(このinstanceを実行するとnull以外になります)
  • に指定します.
    ただし、JVMのインスタント・コンパイラでは、コマンドの再ソートの最適化が存在します.すなわち、上記の2ステップ目と3ステップ目の順序は保証されず、最終的な実行順序は1−2−3であっても1−3−2であってもよい.後者であれば、3実行完了、2未実行の前にスレッド2によってプリエンプトされ、このときinstanceは既にnullではない(ただし初期化されていない)ので、スレッド2は直接instanceに戻り、使用し、そのままエラーを報告します.
    instance変数をvolatileに宣言するだけでいいです.
    public class Singleton {
        private volatile static Singleton instance; //    volatile
        private Singleton (){}
    
        public static Singleton getSingleton() {
            if (instance == null) {                         
                synchronized (Singleton.class) {
                    if (instance == null) {       
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    
    }

    volatileを使用する理由は可視性にあると考えられています.つまり、スレッドがローカルにinstanceのコピーが保存されないことを保証し、毎回メインメモリから読み取ります.しかし、実は間違っています.volatileを使用する主な理由は、命令の再ソート最適化を禁止する別の特性です.すなわち、volatile変数の付与操作の後にメモリバリア(生成されたアセンブリコード)があり、読み取り操作がメモリバリアの前に並べ替えられない.例えば、上記の例では、1−2−3の実行後または1−3−2の実行後に、1−3の実行後に値を取得しなければならない場合がある.「先行発生原則」の観点から理解すると、1つのvolatile変数に対する書き込み操作が先行してこの変数に対する読み取り操作である(ここでの「後続」は時間的な前後順である).
    しかし、Java 5以前のバージョンでvolatileのデュアルロックが使用されていたことに特に注意してください.その理由はJava 5以前のJMM(Javaメモリモデル)に欠陥があり、変数をvolatileと宣言しても完全に並べ替えを避けることができず、主にvolatile変数の前後のコードには並べ替えの問題が残っているためである.このvolatileシールド再ソートの問題はJava 5で修正されたので,その後で安心してvolatileを使用することができる.
    このような複雑で問題を隠す方法が好きではないと信じています.もちろん、スレッドの安全な単一のモードを実現する方法があります.
    餓漢式static final field
    この方法は、単一のインスタンスがstaticおよびfinal変数として宣言されるため、クラスがメモリに最初にロードされると初期化されるため、インスタンスの作成自体はスレッドが安全です.
    public class Singleton{
        //        
        private static final Singleton instance = new Singleton();
    
        private Singleton(){}
    
        public static Singleton getInstance(){
            return instance;
        }
    }

    この書き方が完璧であれば、そんなにダブルチェックの問題をくどくど言う必要はありません.欠点は、クライアントがgetInstance()メソッドを呼び出していない場合でも、クラスのロード後すぐにインスタンスが初期化される怠け者ロードモード(lazy initialization)ではないことです.餓漢的な作成方法は、Singletonインスタンスの作成がパラメータまたはプロファイルに依存しているなど、いくつかのシーンでは使用できません.getInstance()の前にメソッド設定パラメータを呼び出さなければなりません.そうすると、このような単一の書き方は使用できません.
    静的内部クラスstatic nested class
    私は静的内部クラスを使用する傾向がありますが、この方法も『Effective Java』で推奨されています.
    public class Singleton {  
        private static class SingletonHolder {  
            private static final Singleton INSTANCE = new Singleton();  
        }  
        private Singleton (){}  
        public static final Singleton getInstance() {  
            return SingletonHolder.INSTANCE; 
        }  
    }

    この書き方は依然としてJVM自身のメカニズムを用いてスレッドの安全問題を保証している.SingletonHolderはプライベートなのでgetInstance()以外はアクセスできないので、怠け者式です.同時にインスタンスを読み込むときは同期せず、パフォーマンスの欠陥はありません.JDKバージョンにも依存しません.
    Enumの列挙
    列挙して単例を書くのは本当に簡単ですね.これもその最大の利点です.次のコードは、列挙インスタンスを宣言する一般的な方法です.
    public enum EasySingleton{
        INSTANCE;
    }

    EasySingleton.INSTANCEを使用してインスタンスにアクセスできます.これはgetInstance()メソッドを呼び出すよりも簡単です.列挙の作成はデフォルトでスレッドが安全なので、double checked lockingを心配する必要はありません.また、逆シーケンス化による新しいオブジェクトの再作成も防止できます.しかし、このように書かれている人はあまり見られません.よく知らないからかもしれません.
    まとめ
    一般的に、単例モードには5つの書き方がある:怠け者、餓漢、二重検査ロック、静的内部類、列挙.上記はいずれもスレッドセキュリティの実現であり,文章の冒頭に与えられた第1の方法は正確な書き方ではない.
    私個人としては、一般的に餓漢式をそのまま使えば良いのですが、怠惰ロード(lazy initialization)を明確に要求すると静的内部クラスを使う傾向があり、逆シーケンス化に関連してオブジェクトを作成する場合は列挙を用いて単例を実現してみます.
    Read More
    Double Checked Locking on Singleton Class in Java
    Why Enum Singleton are better in Java
    How to create thread safe Singleton in Java
    10 Singleton Pattern Interview questions in Java
    原文住所:http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/