Java単例モードの正確な実現


Java単例モード
単一例のメリット
シングル・インスタンス・モードは、アプリケーションで頻繁に作成されるオブジェクトに適しています.重量レベルのオブジェクトの場合は、シングル・インスタンス・モードを使用する必要があります.たとえば、プロファイルは、シングル・インスタンス・モードを使用しない場合、各プロファイル・オブジェクトの内容が同じであり、重複するオブジェクトを作成すると貴重なメモリが浪費されるため、シングル・インスタンス・モードを使用してパフォーマンスの向上を達成し、メモリのオーバーヘッドとGCの圧力を低減する必要がある.本論文では,正しい単一例モードをどのように実現するかを一歩一歩深く議論する.
単例モードの一般的な書き方
  • 餓漢式
  • public class HungryMode {
    
        private static HungryMode sHungryMode = new HungryMode();
    
        private HungryMode() {
            System.out.println("create " + getClass().getSimpleName());
        }
    
        public static void fun(){
            System.out.println("call fun in HungryMode");
        }
    
        public static HungryMode getInstance(){
            return sHungryMode;
        }
    
        public static void main(String[] args) {
            HungryMode.fun();
        }
    
    }

    餓漢式の単例は、プライベートな構造方法とプライベートな静的現在のクラスインスタンスオブジェクトと共通の静的取得インスタンスメソッドを加えて構成されています.クラスインスタンスオブジェクトは静的変数なので、クラスをロードするときにクラスのインスタンスオブジェクトを作成します.そうすると、メモリが消費され、パフォーマンスが浪費されます.HungryMode.fun()メソッドで検証できます.このメソッドを直接呼び出すと、メモリにHungryModeというクラスがロードされ、静的なクラスインスタンスオブジェクトもインスタンス化されます.だから実行効果は
    create HungryMode
    call fun in HungryMode
    

    このオブジェクトの構造方法が複雑であれば、このような単例の書き方はクラスのロードが遅く、多くの性能を浪費するため、怠惰なロード、いわゆる怠惰なロードが必要です.
  • 怠け者式
  • public class LazyMode {
    
        private static LazyMode sLazyMode;
    
        private LazyMode() {
            System.out.println("create " + getClass().getSimpleName());
        }
    
        public static LazyMode getInstance(){
            if (sLazyMode == null) {
                sLazyMode = new LazyMode();
            }
            return sLazyMode;
        }
    
        public static void main(String[] args) {
            LazyMode.getInstance();
        }
    
    }

    怠け者式は餓漢式に基づいて改良され、クラスインスタンスオブジェクトは怠け者ロード、いわゆる遅延ロードを行うため、いくつかの性能が向上した.
    マルチスレッドの一例
    以上の書き方は、単一スレッドのプログラミング環境では問題ありませんが、複数のスレッドで上記の単一パターンを使用すると、単一パターンの設計原則に反し、複数のオブジェクトが現れます.簡単な方法は、クラスインスタンスを取得する方法に同期ロックを付け、クラスインスタンスオブジェクトにvolatile修飾子を付けることで、volatileはオブジェクトの可視性を保証します.つまり、ワークメモリのコンテンツ更新がすぐにメインメモリに表示されます.ワークメモリはスレッド固有のメモリであり、メインメモリはすべてのスレッドが共有するメモリである.もう1つの役割は、命令の再ソート最適化を禁止することです.私たちが書いたコード(特にマルチスレッドコード)は、コンパイラの最適化によって、実際に実行するときに私たちが書いた順序と異なる可能性があることを知っています.コンパイラは、プログラムの実行結果がソースコードと同じであることを保証するが、実際の命令の順序がソースコードと同じであることを保証しない.これは単一スレッドでは問題ないように見えますが、マルチスレッドが導入されると、この乱順は深刻な問題を引き起こす可能性があります.volatileキーワードは意味的にこの問題を解決することができます.コードに示すように
     public class LazyMode {
    
        private static volatile LazyMode sLazyMode;
    
        private LazyMode() {
            System.out.println("create " + getClass().getSimpleName());
        }
    
        public static LazyMode getInstance(){
            synchronized (LazyMode.class) {
                if (sLazyMode == null) {
                    sLazyMode = new LazyMode();
                }
            }
            return sLazyMode;
        }
    
        public static void main(String[] args) {
            LazyMode.getInstance();
        }
    
    }

    実は上のコードはまだ性能の問題があって、同期ロックのメカニズムのため、複数のスレッドがクラスインスタンスオブジェクトを取得して並んでロックを取得するのを待つ必要はありません.これは必要ありません.多くの場合、クラスインスタンスオブジェクトはすでに作成に成功しているので、ロックされたコードブロックに入る必要はありません.そこで、上のコードが二重検査の単例モードであることを再改善することができます.コードに示すように
    /**
     *         ,        
     */
    public class DoubleCheckMode {
    
        private volatile static DoubleCheckMode sDoubleCheckMode ;
    
        public DoubleCheckMode() {
            System.out.println(" created " + getClass().getSimpleName());
        }
    
        public static DoubleCheckMode getInstance() {
            if (sDoubleCheckMode == null)
                synchronized (DoubleCheckMode.class) {
                    if (sDoubleCheckMode == null) {
                        sDoubleCheckMode = new DoubleCheckMode();
                    }
                }
            return sDoubleCheckMode;
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread() {
                    @Override
                    public void run() {
                        super.run();
                        System.out.println("thread" + getId());
                        DoubleCheckMode.getInstance();
                    }
                }.start();
            }
        }
    
    }

    このような書き方で効率と安全の二重保証ができます.しかし、jdk 1.5以降に正しく動作するまで、命令の再配置最適化を禁止する意味がある.これまでのJDKでは、変数をvolatileと宣言しても、並べ替えの問題を完全に回避することはできませんでした.したがって、jdk 1.5バージョンまでは、二重チェックロック形式の一例モードではスレッドの安全は保証されません.しかし、今のjdk環境の多くは1.5以降で、この問題は私たちが印象を持っていればいいです.
    静的内部クラスインスタンスの使用例
    この実装は,静的クラスが1回しかロードされないメカニズムを利用して,静的内部クラスを用いて単例オブジェクトを持ち,単例の効果を達成し,直接コードをアップロードすることである.
    /**
     *             ,             ,      ,       
     */
    public class InnerStaticMode {
    
        private static class SingleTonHolder {
    
            public static InnerStaticMode sInnerStaticMode = new InnerStaticMode();
    
        }
    
        public static InnerStaticMode getInstance(){
            return SingleTonHolder.sInnerStaticMode;
        }
    
    }
    

    列挙による単一例の実装
    /**
     *            ,Android   
     */
    public enum  EnumMode {
    
        INSTANCE;
    
        private int id;
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    }
    

    まとめ
    列挙は単例を実現し、Androidプラットフォームでの使用を推奨しない.メモリ消費が他の方法で多くなるため、Android公式も列挙を推奨しない.Androidプラットフォームは二重検査や静的内部クラス単例を推奨し、現在のAndroid開発環境jdkは一般的に1.5を超えている.だからvolatileの問題は心配する必要はありません.Javaプラットフォームが開発したEffective Javaでは、列挙を使用して単一の例を実現することを推奨しており、効率性を保証し、逆シーケンス化による新しいオブジェクトの作成の問題も解決できます.
    資料参考:本当に単例モードを書くことができますか?