Maison >Java >javaDidacticiel >Analysons ensemble le singleton du modèle de conception Java

Analysons ensemble le singleton du modèle de conception Java

WBOY
WBOYavant
2022-11-07 16:56:261032parcourir

Cet article vous apporte des connaissances pertinentes sur java, qui présente principalement le contenu pertinent sur le modèle singleton dans le modèle de conception. Le principe de base du singleton est que la classe de l'objet singleton ne sera initialisée qu'une seule fois. ensemble, j'espère que cela sera utile à tout le monde.

Analysons ensemble le singleton du modèle de conception Java

Étude recommandée : "Tutoriel vidéo Java"

Le type d'énumération d'un seul élément est souvent le meilleur moyen d'implémenter Singleton.

Qu'est-ce qu'un singleton ? Comme principe de base, la classe d'un objet singleton ne sera initialisée qu'une seule fois. En Java, on peut dire qu'une seule instance d'objet de la classe existe dans la JVM. Sous Android, on peut dire qu'il n'y a qu'une seule instance d'objet de cette classe lors de l'exécution du programme.

Étapes simples de mise en œuvre du mode singleton :

  • Le constructeur est privé, garantissant que les objets ne peuvent pas être créés de l'extérieur via de nouveaux.

  • fournit des méthodes statiques externes pour obtenir des instances de cette classe.

  • Créez un objet de cette classe à l'intérieur de la classe et renvoyez-le via la méthode statique à l'étape 2.

Suivez les étapes ci-dessus et écrivez un modèle de singleton que vous jugez plus rigoureux, puis voyez si le singleton que vous avez écrit peut remplir les conditions suivantes :

  • Votre singleton est-il chargé à la demande ?
  • Votre singleton est-il thread-safe ? Implique trois éléments de concurrence : atomicité, visibilité, ordre涉及到并发三要素:原子性、可见性、有序性
  • 你的单例暴力反射和序列化安全吗?

一、饿汉式

//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了,相信某些场合枚举所带来的便利远远大于这点所谓的性能影响)

La réflexion violente et la sérialisation de votre singleton sont-elles sûres ? 🎜🎜

1. Style chinois affamé

 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());
 }
🎜🎜Avantages : 🎜Conception simple, résolvant le problème de l'instanciation multithread. 🎜🎜🎜Inconvénients : 🎜🎜Lorsque la machine virtuelle charge la classe SingleTon, les variables statiques de la classe se verront attribuer des valeurs 🎜 lors de la phase d'initialisation, c'est-à-dire lorsque la machine virtuelle charge la classe (la méthode getInstance ne peut pas être appelée à ce moment). L'appel de new SingleTon(); crée une instance de l'objet. Après cela, que l'objet instance soit utilisé ou non, il occupera de l'espace mémoire. 🎜

2. Style Lazy Man

 singleton1.hashCode():1296064247
 singleton2.hashCode():1637070917
🎜🎜Avantages : 🎜Le design est également relativement simple. Contrairement au style Hungry Man, lorsque ce Singleton est chargé, la variable statique modifiée par static sera initialisée à null. , il n'occupera pas de mémoire pour le moment, mais l'objet instance sera initialisé et créé à la demande lorsque la méthode getInstance sera appelée pour la première fois. 🎜🎜🎜Inconvénients : 🎜Il n'y a aucun problème dans un environnement monothread. Dans un environnement multithread, des problèmes de sécurité des threads se produiront. Lorsque deux threads exécutent l'instruction 🎜instane == null🎜 en même temps et que les deux réussissent, ils instancieront chacun un objet, ce n'est donc plus un singleton. 🎜🎜🎜Comment résoudre le problème multi-instance de style paresseux dans un environnement multi-thread ? 🎜🎜🎜🎜🎜Classe interne statique🎜
  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;
     } 
 }
🎜🎜🎜Méthode de synchronisation directe🎜
 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
🎜🎜Avantages :🎜Un seul thread peut instancier l'objet, ce qui résout le problème de sécurité des threads. 🎜🎜🎜Inconvénients : 🎜Pour les méthodes statiques, le mot-clé synchronisé verrouillera toute la classe. Chaque fois que la méthode getInstance est appelée, le thread sera synchronisé, ce qui est très inefficace, de plus, une fois l'objet instance créé, il n'y a pas de synchronisation. besoin de continuer la synchronisation. 🎜🎜🎜Remarque : Le synchronisé ici assure l'atomicité et la visibilité mémoire de l'opération. 🎜🎜🎜🎜🎜Bloc de code synchronisé (double vérification du mode de verrouillage DCL)🎜
 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());
 }
🎜🎜Avantages : 🎜Ajout d'un bloc de code synchronisé, dans le bloc de code synchronisé pour déterminer si l'objet d'instance existe, sinon, allez Créer , cette fois, cela peut en fait résoudre complètement le problème, car bien que plusieurs threads soient utilisés pour obtenir l'objet d'instance, un seul thread entrera dans le bloc de code de synchronisation en même temps. Ensuite, après la création de l'objet à ce moment-là, l'autre même si le. le thread entre à nouveau dans le bloc de code synchronisé, puisque l'objet d'instance a été créé, il peut revenir directement. Mais pourquoi devons-nous juger que l’instance est à nouveau vide à l’étape précédente du bloc de code de synchronisation ? En effet, après avoir créé l'objet instance, nous déterminons directement si l'objet instance est vide. S'il n'est pas vide, il suffit de le renvoyer directement, ce qui évite de saisir à nouveau le bloc de code de synchronisation et améliore les performances. 🎜🎜🎜Inconvénients : 🎜Impossible d'éviter une réflexion violente pour créer des objets. 🎜🎜🎜Remarque : Le volatile joue ici un rôle dans la visibilité de la mémoire et empêche la réorganisation des instructions. 🎜🎜🎜🎜

3. Énumération pour implémenter des singletons

 singleton2.hashCode():1296064247
 singleton1.hashCode():1637070917
🎜L'énumération pour implémenter des singletons est la méthode la plus recommandée, car même par la sérialisation, la réflexion, etc., il n'y a aucun moyen de détruire le singleton. Exemple. (Concernant l'affirmation selon laquelle l'utilisation de l'énumération par Android entraînerait des problèmes de performances, cela devrait être l'ère de la mémoire limitée avant Android 2. En raison de ce soi-disant impact sur les performances)🎜

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

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer