ホームページ  >  記事  >  Java  >  Java デザインパターンのシングルトンを一緒に分析しましょう

Java デザインパターンのシングルトンを一緒に分析しましょう

WBOY
WBOY転載
2022-11-07 16:56:26958ブラウズ

この記事では、java に関する関連知識を提供します。主に、設計パターン内のシングルトン パターンに関する関連内容を紹介します。シングルトンの基本原理は、シングルトン オブジェクトのクラスです。皆さんの参考になれば幸いです。

Java デザインパターンのシングルトンを一緒に分析しましょう

推奨学習: 「java ビデオ チュートリアル

単一要素の列挙型は、多くの場合、シングルトンベストメソッドを実装します。

シングルトンとは何ですか?基本原則として、シングルトン オブジェクトのクラスは 1 回だけ初期化されます。 Java では、JVM 内にクラスのオブジェクト インスタンスが 1 つだけ存在すると言えます。 Android では、プログラムの実行中にこのクラスのオブジェクト インスタンスは 1 つだけ存在すると言えます。

シングルトン モードの簡単な実装手順:

  • 構築メソッドはプライベートであり、外部からオブジェクトを作成できないようにします。新しい。

  • # このクラスのインスタンスを取得するための静的メソッドを提供します。

  • #クラス内にこのクラスのオブジェクトを作成し、手順 2 の静的メソッドを通じてそれを返します。

上記の手順に従って、より厳密だと思われるシングルトン パターンを書き留め、作成したシングルトンが次の条件を満たすかどうかを確認します。

  • シングルトンはオンデマンドでロードされていますか?
  • あなたのシングルトンはスレッドセーフですか?同時実行の 3 つの要素: 原子性、可視性、秩序性が含まれます
  • あなたのシングルトンの激しいリフレクションとシリアル化は安全ですか?

1. 飢えた中華風

//JAVA实现public class SingleTon {    //第三步创建唯一实例
    private static SingleTon instance = new SingleTon();    
    //第一步构造方法私有
    private SingleTon() {
    }    
    //第二步暴露静态方法返回唯一实例
    public static SingleTon getInstance() {        return instance;
    } 
}//Kotlin实现object SingleTon

利点: シンプルな設計で、マルチスレッドのインスタンス化の問題を解決します。

欠点: 仮想マシンが SingleTon クラスをロードすると、初期化フェーズ中、つまり仮想マシンが SingleTon クラスをロードするときに、クラスの静的変数が に割り当てられます。 class (この時点では getInstance メソッドは呼び出されない可能性があります) new SingleTon(); が呼び出され、オブジェクトのインスタンスが作成されます。その後、インスタンス オブジェクトが使用されているかどうかに関係なく、メモリ空間を占有します。

2. Lazy Man スタイル

//JAVA实现public class SingleTon {    //创建唯一实例
    private static SingleTon instance = null;    
    private SingleTon() {
    }    
    public static SingleTon getInstance() {        //延迟初始化 在第一次调用 getInstance 的时候创建对象
        if (instance == null) {
            instance = new SingleTon();
        }        return instance;
    } 
}//Kotlin实现class SingleTon private constructor() {    companion object {        private var instance: SingleTon? = null
            get() {                if (field == null) {
                    field = SingleTon()
                }                return field
            }        fun get(): SingleTon{            return instance!!
        }
    }
}

利点: デザインも比較的シンプルで、Hungry Man スタイルとは異なります。変数は null に初期化されます。この時点ではメモリを占有しません。代わりに、getInstance メソッドが初めて呼び出されたときに、インスタンス オブジェクトが初期化され、オンデマンドで作成されます。

欠点: シングルスレッド環境では問題ありませんが、マルチスレッド環境ではスレッド セーフティの問題が発生します。 2 つのスレッドがステートメント instane == null を同時に実行し、両方が成功すると、それぞれがオブジェクトをインスタンス化するため、そのスレッドはシングルトンではなくなります。

マルチスレッド環境で遅延スタイルのマルチインスタンスの問題を解決するにはどうすればよいですか?

  • #静的内部クラス

    //JAVA实现public class SingleTon {    
        private static class InnerSingleton{        private static SingleTon singleTon  = new SingleTon();
        }    public SingleTon getInstance(){        return InnerSingleton.singleTon;
        }    
        private SingleTon() {
        }
    }//kotlin实现class SingleTon private constructor() {
        companion object {        val instance = InnerSingleton.instance
        }    private object InnerSingleton {        val instance = SingleTon()
        }
    }
  • 直接同期メソッド

    //JAVA实现public class SingleTon {    //创建唯一实例
        private static SingleTon instance = null;    
        private SingleTon() {
        }    
        public static synchronized SingleTon getInstance() {        if (instance == null) {
                instance = new SingleTon();
            }        return instance;
        } 
    }//Kotlin实现class SingleTon private constructor() {  companion object {      private var instance: SingleTon? = null
              get() {              if (field == null) {
                      field = SingleTon()
                  }              return field
              }      @Synchronized
          fun get(): SingleTon{          return instance!!
          }
      }
    }

    利点: 1 つのスレッドのみがロックを通じてオブジェクトをインスタンス化できるため、スレッドの安全性の問題は解決されます。

    欠点: 静的メソッドの場合、synchronized キーワードはクラス全体をロックします。getInstance メソッドが呼び出されるたびにスレッドが同期されるため、非常に非効率的です。インスタンス オブジェクトが作成されるため、同期を続ける必要はありません。

    備考: ここで同期することで、操作のアトミック性とメモリの可視性が保証されます。

  • 同期コード ブロック (ダブル チェック ロック モード DCL)

    //JAVA实现 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;
        } 
    }//kotlin实现class SingleTon private constructor() {    companion object {        val instance: SingleTon by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
                SingleTon() 
            }
      }
    }
    或者class SingleTon private constructor() {    companion object {        @Volatile private var instance: SingleTon? = null
            fun getInstance() =
                  instance ?: synchronized(this) {
                      instance ?: SingleTon().also { instance = it }
                  }
      }
    }

    利点: 同期コード ブロックを追加しました。インスタンス オブジェクトが同期コード ブロックに存在するかどうかを確認し、存在しない場合は作成します。インスタンス オブジェクトを取得するために複数のスレッドが使用されますが、同時に存在するのは 1 つだけであるため、これにより問題は実際に解決できます。スレッドは同期コードブロックに入るので、この時点でオブジェクトが作成された後、他のスレッドが再び同期コードブロックに入っても、インスタンスオブジェクトが作成されているため、そのまま戻ることができます。しかし、なぜ同期コード ブロックの前のステップでインスタンスが空であると再度​​判断する必要があるのでしょうか?これは、インスタンス オブジェクトを作成した後、インスタンス オブジェクトが空かどうかを直接判断し、空でない場合は直接返すだけで、同期コード ブロックに再度入ることがなくなり、パフォーマンスが向上します。

    欠点: オブジェクトを作成する際に激しい反射を避けることができません。

    備考: ここでの volatile は、メモリの可視性と命令の並べ替えの防止に役割を果たします。

3. シングルトンを実装するための列挙

public enum SingletonEnum {    INSTANCE;    public static void main(String[] args) {        System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
    }
}

シングルトンを実装するための列挙は、シリアル化やリフレクションなどを介しても実行できるため、最も推奨される方法です。単一性を破壊することはできません。

(Android の列挙型の使用がパフォーマンスの問題を引き起こすという記述については、Android 2 より前のメモリ不足の時代のはずです。このいわゆるパフォーマンスへの影響が原因です)

四、如何避免单例模式反射攻击

以最初的DCL为测试案例,看看如何进行反射攻击及又如何在一定程度上避免反射攻击。反射攻击代码如下:

 public static void main(String[] args) {

     SingleTon singleton1 = SingleTon.getInstance();
     SingleTon singleton2 = null;

     try {
         Class<SingleTon> clazz = SingleTon.class;
         Constructor<SingleTon> 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.imock.demo.TestUtil.testSingleInstance(TestUtil.java:45)
 at com.imock.demo.TestUtil.main(TestUtil.java:33)
 Caused by: java.lang.RuntimeException: 单例构造器禁止反射调用
 at com.imock.demo.SingleTon.<init>(SingleTon.java:16)
 ... 6 more Exception in thread "main" java.lang.NullPointerException
 at com.imock.demo.TestUtil.testSingleInstance(TestUtil.java:49)
 at com.imock.demo.TestUtil.main(TestUtil.java:33) 
 Process finished with exit code 1

然后我们把上述测试代码修改如下(调换了singleton1的初始化顺序)

 public static void main(String[] args) {
     SingleTon singleton2 = null;

     try {
         Class<SingleTon> clazz = SingleTon.class;
         Constructor<SingleTon> 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方法内部判断枚举抛异常)

推荐学习:《java视频教程

以上がJava デザインパターンのシングルトンを一緒に分析しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.imで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。