Heim  >  Artikel  >  Java  >  Lassen Sie uns gemeinsam den Singleton des Java-Entwurfsmusters analysieren

Lassen Sie uns gemeinsam den Singleton des Java-Entwurfsmusters analysieren

WBOY
WBOYnach vorne
2022-11-07 16:56:26958Durchsuche

Dieser Artikel vermittelt Ihnen relevantes Wissen über Java, das hauptsächlich den relevanten Inhalt des Singleton-Musters im Entwurfsmuster vorstellt. Das Grundprinzip von Singleton besteht darin, dass die Klasse des Singleton-Objekts nur einmal initialisiert wird Wenn wir gemeinsam daran arbeiten, hoffe ich, dass es für alle hilfreich sein wird.

Lassen Sie uns gemeinsam den Singleton des Java-Entwurfsmusters analysieren

Empfohlene Studie: „Java-Video-Tutorial

Der Aufzählungstyp eines einzelnen Elements ist oft der beste Weg, Singleton zu implementieren.

Was ist ein Singleton? Als Grundprinzip gilt: Die Klasse eines Singleton-Objekts wird nur einmal initialisiert. In Java können wir sagen, dass in der JVM nur eine Objektinstanz der Klasse existiert. In Android können wir sagen, dass es während der Ausführung des Programms nur eine Objektinstanz dieser Klasse gibt.

Einfache Implementierungsschritte des Singleton-Modus:

  • Der Konstruktor ist privat und stellt sicher, dass Objekte nicht von außen durch neue erstellt werden können.

  • stellt externe statische Methoden bereit, um Instanzen dieser Klasse abzurufen.

  • Erstellen Sie ein Objekt dieser Klasse innerhalb der Klasse und geben Sie es über die statische Methode in Schritt 2 zurück.

Befolgen Sie die obigen Schritte, um ein Singleton-Muster aufzuschreiben, das Ihrer Meinung nach strenger ist, und prüfen Sie dann, ob der von Ihnen geschriebene Singleton die folgenden Bedingungen erfüllen kann:

  • Wird Ihr Singleton bei Bedarf geladen?
  • Ist Ihr Singleton-Thread-sicher? Beinhaltet drei Elemente der Parallelität: Atomizität, Sichtbarkeit, Ordnung涉及到并发三要素:原子性、可见性、有序性
  • 你的单例暴力反射和序列化安全吗?

一、饿汉式

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

优点:设计简单 ,解决了多线程实例化的问题。

缺点:在虚拟机加载SingleTon类的时候,将会在初始化阶段为类静态变量赋值,也就是在虚拟机加载该类的时候(此时可能并没有调用 getInstance 方法)就已经调用了 new SingleTon(); 创建了该对象的实例,之后不管这个实例对象用不用,都会占据内存空间。

二、懒汉式

//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!!
        }
    }
}

优点:设计也是比较简单的,和饿汉式不同,当这个Singleton被加载的时候,被static修饰的静态变量将会被初始化为null,这个时候并不会占用内存,而是当第一次调用getInstance方法的时候才会被初始化实例对象,按需创建。

缺点:在单线程环境下是没有问题的,在多线程环境下,会产生线程安全问题。在有两个线程同时 运行到了 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!!
          }
      }
    }

    优点:加锁只有一个线程能实例该对象,解决了线程安全问题。

    缺点:对于静态方法而言,synchronized关键字会锁住整个 Class,每次调用getInstance方法都会线程同步,效率十分低下,而且当创建好实例对象之后,也就不必继续进行同步了。

    备注:此处的synchronized保证了操作的原子性和内存可见性。

  • 同步代码块(双重检锁方式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 }
                  }
      }
    }

    优点:添加了一个同步代码块,在同步代码块中去判断实例对象是否存在,如果不存在则去创建,这个时候其实就完全可以解决问题了,因为虽然是多个线程去获取实例对象,但是在同一个时间也只会有一个线程会进入到同步代码块,那么这个时候创建好对象之后,其他线程即便再次进入同步代码块,由于已经创建好了实例对象,便直接返回即可。但是为什么还要在同步代码块的上一步再次去判断instance为空呢?这个是由于当我们创建好实例对象之后,直接去判断此实例对象是否为空,如果不为空,则直接返回就好了,就避免再次进去同步代码块了,提高了性能。

    缺点:无法避免暴力反射创建对象。

    备注:此处的volatile发挥了内存可见性及防止指令重排序作用。

三、枚举实现单例

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

枚举实现单例是最为推荐的一种方法,因为就算通过序列化,反射等也没办法破坏单例性。(关于Android使用枚举会产生性能问题的说法,这应该是Android 2.x系统之前内存紧张的时代了,现在已经Android 13了,相信某些场合枚举所带来的便利远远大于这点所谓的性能影响)

Ist Ihre Singleton-gewalttätige Reflexion und Serialisierung sicher? 🎜🎜

1. Hungry Chinese Style

 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());
 }
🎜🎜Vorteile: 🎜Einfaches Design, das das Problem der Multithread-Instanziierung löst. 🎜🎜🎜Nachteile: 🎜🎜Wenn die virtuelle Maschine die SingleTon-Klasse lädt, werden den statischen Variablen der Klasse während der Initialisierungsphase, dh wenn die virtuelle Maschine die Klasse lädt, Werte zugewiesen 🎜 (die getInstance-Methode wird möglicherweise nicht aufgerufen). Zu diesem Zeitpunkt wird durch den Aufruf von new SingleTon(); eine Instanz des Objekts erstellt, unabhängig davon, ob das Instanzobjekt verwendet wird oder nicht. 🎜

2. Lazy Man Style

 singleton1.hashCode():1296064247
 singleton2.hashCode():1637070917
🎜🎜Vorteile: 🎜Das Design ist auch relativ einfach, wenn dieser Singleton geladen wird, wird die durch static geänderte Variable auf Null initialisiert. , es wird zu diesem Zeitpunkt keinen Speicher belegen, aber das Instanzobjekt wird bei Bedarf initialisiert und erstellt, wenn die getInstance-Methode zum ersten Mal aufgerufen wird. 🎜🎜🎜Nachteile: 🎜In einer Single-Thread-Umgebung gibt es kein Problem. In einer Multi-Thread-Umgebung treten Thread-Sicherheitsprobleme auf. Wenn zwei Threads die Anweisung 🎜instane == null🎜 gleichzeitig ausführen und beide bestehen, instanziiert jeder Thread ein Objekt, sodass es sich nicht mehr um einen Singleton handelt. 🎜🎜🎜Wie löst man das Lazy-Style-Multiinstanzproblem in einer Multithread-Umgebung? 🎜🎜🎜🎜🎜Statische innere Klasse🎜
  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;
     } 
 }
🎜🎜🎜Direkte Synchronisationsmethode🎜
 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
🎜🎜Vorteile:🎜Nur ein Thread kann das Objekt instanziieren, wodurch das Thread-Sicherheitsproblem gelöst wird. 🎜🎜🎜Nachteile: 🎜Bei statischen Methoden sperrt das synchronisierte Schlüsselwort die gesamte Klasse. Bei jedem Aufruf der getInstance-Methode wird der Thread synchronisiert, was außerdem sehr ineffizient ist Sie müssen die Synchronisierung fortsetzen. 🎜🎜🎜Hinweis: Die Synchronisierung hier stellt die Atomizität und Speichersichtbarkeit des Vorgangs sicher. 🎜🎜🎜🎜🎜Synchronisierter Codeblock (Sperrmodus-DCL mit doppelter Überprüfung)🎜
 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());
 }
🎜🎜Vorteile: 🎜Einen synchronisierten Codeblock hinzugefügt. Im synchronisierten Codeblock können Sie feststellen, ob das Instanzobjekt vorhanden ist. Wenn nicht, gehen Sie zu Erstellen Diesmal kann das Problem tatsächlich vollständig gelöst werden, da zwar mehrere Threads zum Abrufen des Instanzobjekts verwendet werden, jedoch nur ein Thread gleichzeitig in den Synchronisationscodeblock eintritt. Dann wird das Objekt zu diesem Zeitpunkt erstellt Der Thread betritt den synchronisierten Codeblock erneut. Da das Instanzobjekt erstellt wurde, kann er direkt zurückkehren. Aber warum müssen wir im vorherigen Schritt des Synchronisationscodeblocks erneut feststellen, dass die Instanz leer ist? Dies liegt daran, dass wir nach dem Erstellen des Instanzobjekts direkt feststellen, ob das Instanzobjekt leer ist. Wenn es nicht leer ist, geben wir es einfach direkt zurück, wodurch eine erneute Eingabe des Synchronisierungscodeblocks vermieden und die Leistung verbessert wird. 🎜🎜🎜Nachteile: 🎜Bei der Erstellung von Objekten kann heftige Reflexion nicht vermieden werden. 🎜🎜🎜Bemerkungen: Das flüchtige Element spielt hier eine Rolle bei der Speichersichtbarkeit und verhindert die Neuordnung von Anweisungen. 🎜🎜🎜🎜

3. Aufzählung zur Implementierung von Singletons

 singleton2.hashCode():1296064247
 singleton1.hashCode():1637070917
🎜Die Aufzählung zur Implementierung von Singletons ist die am meisten empfohlene Methode, da es selbst durch Serialisierung, Reflexion usw. keine Möglichkeit gibt, sie zu zerstören Singleton. Beispiel. (Bezüglich der Aussage, dass die Verwendung der Aufzählung durch Android zu Leistungsproblemen führen wird, sollte dies die Ära des knappen Speichers vor Android 2 sein. Aufgrund dieser sogenannten Leistungsbeeinträchtigung)🎜

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

以最初的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视频教程

Das obige ist der detaillierte Inhalt vonLassen Sie uns gemeinsam den Singleton des Java-Entwurfsmusters analysieren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:juejin.im. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen