あなたが理解すべき単一のパターン

9655 ワード

単一要素の列挙タイプはSingletonを実現するための最良の方法となることが多い.
単例とは何ですか.1つの基本原則では、単一のオブジェクトのクラスは1回だけ初期化されます.Javaでは、JVMにはクラスの唯一のオブジェクトインスタンスしか存在しないと言えます.Androidでは、プログラムの実行中にクラスにオブジェクトインスタンスが1つしかないと言えます.
シングル・インスタンス・モードの簡単な実装手順:
  • 構築メソッドはプライベートであり、外部からnewでオブジェクトを作成できないことを保証します.
  • は、クラスインスタンスを取得するための静的方法を対外的に提供する.
  • クラスの内部にクラスのオブジェクトが作成され、2ステップ目の静的メソッドで返されます.

  • 上記の手順に従って、厳密だと思う単例モードを書いて、書いた単例が以下の条件を満たすことができるかどうかを見てみましょう.
  • あなたの単例はオンデマンドでロードされますか?
  • 単一スレッドは安全ですか? : 、 、
  • あなたの単一の暴力反射とシーケンス化は安全ですか?

  • 単一の例が上記の条件を満たしている場合は、上記の条件を満たしていないか、興味のある場合は、以下のいくつかの単一の例を作成する方法から、対応する実装方法の長所と短所と最適化方法を説明します.
  • 餓漢モード
  • 怠け者モード
  • 遅延ロード
  • 静的内部クラス
  • 二重ロック(DCL)
  • 列挙実装例
  • 一、餓漢式
    public class SingleTon {
        //         
        private static SingleTon instance = new SingleTon();
        
        //         
        private SingleTon() {
        }
        
        //               
        public static SingleTon getInstance() {
            return instance;
        } 
    }
    

    利点:設計が簡単で、マルチスレッドのインスタンス化の問題を解決しました.
    欠点:SingleTonクラスが仮想マシンにロードされると、初期化フェーズでクラスの静的変数に値が割り当てられます.つまり、仮想マシンがクラスをロードしたとき(getInstanceメソッドが呼び出されていない可能性があります)new SingleTon();が呼び出され、オブジェクトのインスタンスが作成された後、このインスタンスオブジェクトが使用されなくてもメモリ領域が占有されます.
    二、怠け者式
    public class SingleTon {
        //      
        private static SingleTon instance = null;
        
        private SingleTon() {
        }
        
        public static SingleTon getInstance() {
            //             getInstance        
            if (instance == null) {
                instance = new SingleTon();
            }
            return instance;
        } 
    }
    

    利点:設計も比較的簡単で、餓漢式とは異なり、このSingletonがロードされるとstaticで修飾された静的変数がnullに初期化され、このときメモリを消費するのではなく、getInstanceメソッドを初めて呼び出すとインスタンスオブジェクトが初期化され、必要に応じて作成されます.
    欠点:単一スレッド環境では問題なく、マルチスレッド環境ではスレッドセキュリティの問題が発生します.2つのスレッドがinstane==nullという文に同時に実行され、いずれも通過すると、それぞれ1つのオブジェクトがインスタンス化され、単一の例ではありません.
  • 静的内部クラス
    public class SingleTon {
        
        private static class InnerStaticClass{
            private static SingleTon singleTon  = new SingleTon();
        }
    
        public SingleTon getInstance(){
            return InnerStaticClass.singleTon;
        }
        
        private SingleTon() {
        }
    }
    
  • 直接同期方法
    public class SingleTon {
        //      
        private static SingleTon instance = null;
        
        private SingleTon() {
        }
        
        public static synchronized SingleTon getInstance() {
            if (instance == null) {
                instance = new SingleTon();
            }
            return instance;
        } 
    }
    
    の利点:ロックはオブジェクトをインスタンス化できるスレッドが1つしかなく、スレッドセキュリティの問題を解決する.欠点:静的メソッドではsynchronizedキーワードがClass全体をロックし、getInstanceメソッドを呼び出すたびにスレッドが同期し、効率が非常に低下し、インスタンスオブジェクトを作成した後も同期を継続する必要はありません.備考: synchronized 。
  • 同期コードブロック(二重ロック方式DCL)
    public class SingleTon {
        //      
        private static volatile SingleTon instance = null;
        
        private SingleTon() {
        }
        
        public static SingleTon getInstance() {
            if (instance == null) {
                synchronized (SingleTon.class) {   
                    if (instance == null) {
                        instance = new SingleTon();
                    }
                }
            }
            return instance;
        } 
    }
    
    利点:1つの同期コードブロックを追加して、同期コードブロックの中でインスタンスオブジェクトが存在するかどうかを判断して、存在しないならば作成して、この時実は完全に問題を解決することができて、複数のスレッドが実例オブジェクトを取得するのですが、しかし同じ時間に1つのスレッドだけが同期コードブロックに入ることができますでは、このときにオブジェクトを作成した後、他のスレッドが再び同期コードブロックに入っても、インスタンスオブジェクトが作成されているので、そのまま戻ることができます.しかし、なぜ同期コードブロックの前のステップでinstanceが空であると再判断するのでしょうか.これは、インスタンスオブジェクトを作成した後、そのインスタンスオブジェクトが空であるかどうかを直接判断し、空でない場合はそのまま戻るとよいため、同期コードブロックに再び入ることを回避し、パフォーマンスを向上させることができるからである.欠点:暴力的な反射によるオブジェクトの作成は避けられません.備考: volatile 。
  • 三、列挙実現単例
    public enum SingletonEnum {
        INSTANCE;
    
        public static void main(String[] args) {
            System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
        }
    }
    

    列挙して単例を実現することは最も推奨される方法であり,シーケンス化,反射などによっても単例性を破壊することはできないからである.( Android , Android 2.x , Android Q , )
    四.単一モードの反射攻撃を回避する方法
    最初のDCLをテスト例として、反射攻撃コードを見てみましょう.
     public static void main(String[] args) {
    
         SingleTon singleton1 = SingleTon.getInstance();
         SingleTon singleton2 = null;
    
         try {
             Class clazz = SingleTon.class;
             Constructor constructor = clazz.getDeclaredConstructor();
             constructor.setAccessible(true);
             singleton2 = constructor.newInstance();
         } catch (Exception e) {
             e.printStackTrace();
         }
    
         System.out.println("singleton1.hashCode():" + singleton1.hashCode());
         System.out.println("singleton2.hashCode():" + singleton2.hashCode());
     }
    

    実行結果:
     singleton1.hashCode():1296064247
     singleton2.hashCode():1637070917
    

    実行結果から,反射により単一例が破壊されることが分かった.反射の安全をどのように保証しますか?暴制暴でしかできません.すでにインスタンスが存在する場合、コンストラクション関数を呼び出して直接異常を放出し、コンストラクション関数を次のように変更します.
      public class SingleTon {
         //      
         private static volatile SingleTon instance = null;
       
         private SingleTon() {
             if (instance != null) {
                 throw new RuntimeException("           ");
             }
         }
       
         public static SingleTon getInstance() {
             if (instance == null) {
               synchronized (SingleTon.class) {   
                   if (instance == null) {
                       instance = new SingleTon();
                   }
               }
           }
           return instance;
         } 
     }
    

    このとき、反射攻撃を防ぐことができ、異常を放出するのは以下の通りです.
     java.lang.reflect.InvocationTargetException
     at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
     at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
     at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
     at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
     at com.liujc.demo.TestUtil.testSingleInstance(TestUtil.java:45)
     at com.liujc.demo.TestUtil.main(TestUtil.java:33)
     Caused by: java.lang.RuntimeException:            
     at com.liujc.demo.SingleTon.(SingleTon.java:16)
     ... 6 more
     Exception in thread "main" java.lang.NullPointerException
     at com.liujc.demo.TestUtil.testSingleInstance(TestUtil.java:49)
     at com.liujc.demo.TestUtil.main(TestUtil.java:33) 
     Process finished with exit code 1
    

    次に、上記のテストコードを次の( singleton1 )に変更します.
     public static void main(String[] args) {
         SingleTon singleton2 = null;
    
         try {
             Class clazz = SingleTon.class;
             Constructor constructor = clazz.getDeclaredConstructor();
             constructor.setAccessible(true);
             singleton2 = constructor.newInstance();
         } catch (Exception e) {
             e.printStackTrace();
         }
    
         System.out.println("singleton2.hashCode():" + singleton2.hashCode());
    
         SingleTon singleton1 = SingleTon.getInstance(); //     ,       
         System.out.println("singleton1.hashCode():" + singleton1.hashCode());
     }
    

    実行結果:
     singleton2.hashCode():1296064247
     singleton1.hashCode():1637070917
    

    この防御が機能していないことがわかりました.
    欠点:
  • 反射攻撃が通常getInstanceを呼び出す前に発生した場合、反射攻撃のたびに単一のクラスのインスタンスを取得できます.プライベートコンストラクタで静的メンバー(instance)が使用されていても、クラスの初期化段階でインスタンス化されていない単一オブジェクトのため、防御コードは有効ではなく、コンストラクタの反射呼び出しによって単一クラスの複数のインスタンスを作成することができる.
  • 反射攻撃が正常に呼び出された後に発生した場合、防御コードは有効になります.

  • シーケンス化攻撃を回避するにはどうすればいいですか?逆シーケンス化された論理を修正するだけでよい.すなわち、readResolve()メソッドを書き換え、統一インスタンスを返す.
       protected Object readResolve() {
           return getInstance();
       }
    

    脆弱な単例モードは多くの試練を経て,完全体,遅延負荷,スレッド安全,反射およびシーケンス化安全に進化した.簡易コードは次のとおりです.
  • 餓漢モード
    public class SingleTon {
        private static SingleTon instance = new SingleTon();
        
        private SingleTon() {
            if (instance != null) {
                  throw new RuntimeException("           ");
             }
        }
        public static SingleTon getInstance() {
            return instance;
        } 
    }
    
  • 静的内部クラス
    public class SingleTon {
        
        private static class InnerStaticClass{
            private static SingleTon singleTon  = new SingleTon();
        }
    
        public SingleTon getInstance(){
            return InnerStaticClass.singleTon;
        }
        
        private SingleTon() {
           if (InnerStaticClass.singleTon != null) {
                  throw new RuntimeException("           ");
           }
        }
    }
    
  • 怠け者モード
    public class SingleTon {
        //      
        private static SingleTon instance = null;
        
        private SingleTon() {
                if (instance != null) {
                  throw new RuntimeException("           ");
             }
        }
        
        public static SingleTon getInstance() {
            //             getInstance        
            if (instance == null) {
                instance = new SingleTon();
            }
            return instance;
        } 
    }
    
    欠点:
  • 反射攻撃が通常getInstanceを呼び出す前に発生した場合、反射攻撃のたびに単一のクラスのインスタンスを取得できます.プライベートコンストラクタで静的メンバー(instance)が使用されていても、クラスの初期化段階でインスタンス化されていない単一オブジェクトのため、防御コードは有効ではなく、コンストラクタの反射呼び出しによって単一クラスの複数のインスタンスを作成することができる.
  • 反射攻撃が正常に呼び出された後に発生した場合、防御コードは有効になります.


  • (列挙実装単例は最も推奨される方法であり、シーケンス化、反射などによっても単例性を破壊することはできないため、下層実装、例えばnewInstance法内部で列挙放出異常を判断する)