Modèle de conception - connaissez-vous vraiment le modèle de singleton?

Le modèle de conception (modèle de conception) est un ensemble d'expérience de conception de code qui a été utilisé à plusieurs reprises et est connu de la plupart des gens. L'utilisation de modèles de conception permet de réutiliser du code, de le rendre plus facile à comprendre pour les autres et d'assurer la fiabilité du code. Il ne fait aucun doute que les modèles de conception sont gagnant-gagnant pour les autres et les systèmes. Les modèles de conception font de la préparation de code une véritable ingénierie. Les modèles de conception sont la pierre angulaire de l'ingénierie logicielle, tout comme les briques d'un bâtiment. L'utilisation raisonnable de modèles de conception dans le projet peut parfaitement résoudre de nombreux problèmes. Chaque modèle a un principe correspondant qui lui correspond maintenant. Chaque modèle décrit un problème qui se reproduit autour de nous et le cœur du problème. Solution, c'est pourquoi il peut être largement utilisé. En termes simples, un modèle est une solution générale à un certain type de problème dans certains scénarios.

D'une manière générale, les 23 modèles de conception peuvent être à peu près divisés en trois catégories: les modèles de création , les modèles structurels et les modèles de comportement .

Mode de création : le mode d' instanciation d'objet, qui est utilisé pour découpler le processus d'instanciation des objets.

Mode structurel : combinez des classes ou des objets ensemble pour former une structure plus grande.

Modèles de comportement : comment les classes et les objets interagissent et répartissent les responsabilités et les algorithmes.

Aujourd'hui, nous allons d'abord parler du mode singleton en mode création .

Quel est le motif singleton

Le mode singleton est également un problème courant dans les entretiens, même si beaucoup de gens le savent, beaucoup de gens ne sont pas familiers avec toutes les méthodes d'écriture, ainsi que les problèmes d'écriture et les solutions.

Le mode singleton a trois caractéristiques typiques: 1. Il n'y a qu'une seule instance. 2. Auto-instanciation. 3. Fournissez des points d'accès globaux.

Par conséquent, lorsqu'un seul objet d'instance est nécessaire dans le système ou qu'un seul point d'accès public est autorisé dans le système et que l'instance n'est pas accessible via d'autres points d'accès à l'exception de ce point d'accès public, le mode singleton peut être utilisé.

Le principal avantage du mode singleton est d'économiser les ressources système et d'améliorer l'efficacité du système, tout en étant en mesure de contrôler strictement l'accès des clients à celui-ci. C'est peut-être parce qu'il n'y a qu'une seule instance dans le système, ce qui conduit à la surcharge de la classe singleton, qui viole le "principe de responsabilité unique", et qu'il n'y a pas de classe abstraite, donc il est difficile de se développer.

Différentes façons d'écrire en mode singleton

1. Style chinois affamé (recommandé)

De cette façon, private static final Singleton1 INSTANCE = new Singleton1()une variable statique est définie. Lorsque la JVM charge une classe, un singleton sera instancié. Puisqu'un fichier de classe ne sera chargé qu'une seule fois, la JVM peut garantir la sécurité des threads.

En même temps, c'est private Singleton1()un constructeur privatisé, qui peut garantir que cette classe ne peut pas être nouvelle dans d'autres classes.

Si la classe externe veut obtenir cette instance, elle doit être obtenue via une public static Singleton1 getInstance()méthode.

package com.wuxiaolong.design.pattern.singleton;

/**
 * Description: 饿汉式
 * @author 诸葛小猿
 * @date 2020-08-05
 */
public class Singleton1 {
    
    

    /**
     * 静态变量 启动时自动加载
     */
    private static final Singleton1 INSTANCE = new Singleton1();

    /**
     * 私有化构造器,不能new
     */
    private Singleton1(){
    
    }

    /**
     * 对外暴露一个方法,获取同一个实例
     */
    public static Singleton1 getInstance(){
    
    
        return INSTANCE;
    }

    /**
     * 测试
     */
    public static void main(String[] args) {
    
    
        Singleton1 s1 = Singleton1.getInstance();
        Singleton1 s2 = Singleton1.getInstance();

        if(s1 == s2){
    
    
            System.out.println("s1和s2是内存地址相同,是同一个实例");
        }
    }

}

Le seul problème avec cette approche est qu'elle sera instanciée si elle n'est pas utilisée. Si vous ne trouvez pas de défauts, ce n'est pas vraiment un problème.

Cette méthode est simple et pratique, et je la recommande vivement pour être utilisée dans des projets réels .

2. Un homme paresseux

Le style de l'homme paresseux consiste à résoudre le problème ci-dessus du chargement du style de l'homme affamé, qu'il soit utilisé ou non.

Le soi-disant chargement paresseux est lorsqu'il est utilisé, il est initialisé lorsqu'il est utilisé et il ne sera pas chargé lorsqu'il n'est pas utilisé.

Utilisez private static volatile Singleton2 INSTANCEdes variables définies, mais n'initialisera pas l'instance. La variable est initialisée getInstance()lorsque la méthode est appelée .

package com.wuxiaolong.design.pattern.singleton;

import java.util.HashSet;
import java.util.Set;

/**
 * Description: 懒汉式一
 * @author 诸葛小猿
 * @date 2020-08-05
 */
public class Singleton2 {
    
    
    /**
     * 先不初始化   使用volatile的原因见底部
     */
    private static volatile Singleton2 INSTANCE;

    /**
     * 私有化构造器,不能new
     */
    private Singleton2(){
    
    }

    /**
     * 对外暴露一个方法,获取同一个实例。只有实例不存在时才初始化
     * 问题:多线程同时访问getInstance时,可能会new出多个实例
     */
    public static Singleton2 getInstance(){
    
    
        if(INSTANCE == null){
    
    

//            try {
    
    
//                Thread.sleep(10);
//            }catch (Exception e){
    
    
//             e.printStackTrace();
//            }

            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }

    /**
     * 测试
     * 测试时可以开启 getInstance()方法中的sleep
     */
    public static void main(String[] args) {
    
    

        for(int i=0;i<100;i++){
    
    
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    Singleton2 temp = getInstance();
                    // 打印出内存地址,如果内存地址不一样,则生成了多个实例
                    System.out.println(temp.toString());
                }
            }).start();
        }

    }
}

L'utilisation de l'homme paresseux ci-dessus pose également de nombreux autres problèmes. Dans le cas d'un accès multithread getInstance(), il y aura des problèmes de sécurité des threads et plusieurs instances seront générées. Vous pouvez utiliser le maintest ci-dessus pour le tester.

Afin de résoudre le problème de sécurité des threads, vous pouvez utiliser le verrou synchronisé (la méthode statique pour verrouiller consiste à verrouiller cette classe):

    public static synchronized Singleton2 getInstance(){
    
    
        if(INSTANCE == null){
    
    
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }

Mais cela entraînera des problèmes de performances. Afin d'améliorer les performances, ajoutez un verrou au bloc de code:

    public static Singleton3 getInstance(){
    
    
        if(INSTANCE == null){
    
    
            synchronized (Singleton3.class){
    
    
                INSTANCE = new Singleton3();
            }
        }
        return INSTANCE;
    }

Mais cette approche a toujours des problèmes de sécurité des threads. Plusieurs threads peuvent entrer le if en même temps. Bien qu'un seul thread puisse exécuter le bloc de code synchronisé en même temps, les threads entrant le if seront tous instanciés.

3. Paresseux style deux

Afin de résoudre le problème de sécurité des threads du paresseux ci-dessus, quelqu'un a proposé une technique "intelligente": Verrouillage à double vérification (verrouillage à double vérification)

package com.wuxiaolong.design.pattern.singleton;

/**
 * Description: 懒汉式二
 *
 * @author 诸葛小猿
 * @date 2020-08-05
 */
public class Singleton3 {
    
    
    /**
     * 先不初始化
     */
    private static volatile Singleton3 INSTANCE;

    /**
     * 私有化构造器,不能new
     */
    private Singleton3(){
    
    }

    /**
     * 对外暴露一个方法,获取同一个实例
     * 内部双重判断,使用两次INSTANCE == null的判断
     */
    public static Singleton3 getInstance(){
    
    
        if(INSTANCE == null){
    
    
            synchronized (Singleton3.class){
    
    
                // synchronized块内部,再判断一次是否为空
                if(INSTANCE == null){
    
    
                    INSTANCE = new Singleton3();
                }
            }
        }
        return INSTANCE;
    }

    /**
     * 测试
     */
    public static void main(String[] args) {
    
    

        for(int i=0;i<100;i++){
    
    
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    Singleton3 temp = getInstance();

                    // 打印出内存地址,如果内存地址不一样,则生成了多个实例
                    System.out.println(temp.toString());
                }
            }).start();
        }
    }
}

Une double vérification consiste à synchronizedjuger une fois à l'intérieur et à l'extérieur séparément INSTANCE == null, de sorte que la sécurité du fil puisse être garantie:

    public static Singleton3 getInstance(){
    
    
        // 第一次检查
        if(INSTANCE == null){
    
    
            synchronized (Singleton3.class){
    
    
                // synchronized块内部,第二次检查
                if(INSTANCE == null){
    
    
                    INSTANCE = new Singleton3();
                }
            }
        }
        return INSTANCE;
    }

Est-il nécessaire pour la première INSTANCE == null ci-dessus? Bien sûr, s'il n'y a pas de premier jugement, chaque thread sera verrouillé à son entrée, ce qui sera très gourmand en performances; avec le premier jugement, de nombreux threads n'obtiendront pas le verrou.

C'est l'une des méthodes d'écriture les plus parfaites, et il est recommandé de l'utiliser dans des projets réels; en utilisant le double jugement, il n'y a pas de problème de sécurité des threads.

Mais personnellement, ce n'est vraiment pas nécessaire, utilisez simplement le mode homme affamé. Cela peut être recommandé par la Vierge.

4. Paresseux type trois (recommandé)

La classe interne statique est utilisée ici private static class InnerSingleton, ce qui garantit non seulement le chargement paresseux, mais également la sécurité des threads.

La raison du chargement paresseux ici est que lorsque jvm charge Singleton4.class, il ne chargera pas la classe interne statique InnerSingleton.class; seul l'appel getInstance()chargera la classe interne statique InnerSingleton.class

La sécurité des threads ici est garantie par la JVM: lorsque jvm charge Singleton4.class, il ne sera chargé qu'une seule fois; donc InnerSingleton.class n'est chargé qu'une seule fois, donc INSTANCE n'est instancié qu'une seule fois.

package com.wuxiaolong.design.pattern.singleton;

/**
 * Description: 
 * 既保证了懒加载,也保证了线程安全。
 * @author 诸葛小猿
 * @date 2020-08-05
 */
public class Singleton4 {
    
    

    /**
     * 私有化构造器,不能new
     */
    private Singleton4(){
    
    }

    /**
     * 使用静态内部类,jvm加载Singleton4.class的时候,不会加载静态内部类InnerSingleton.class
     * 只有调用getInstance()才会加载静态内部类InnerSingleton.class
     */
    private static class InnerSingleton{
    
    
        private static final Singleton4 INSTANCE = new Singleton4();
    }

    /**
     * 对外暴露一个方法,获取同一个实例
     * 返回的是静态内部类的成员变量,这个变量在调用的时候才会初始化
     */
    public static Singleton4 getInstance(){
    
    
        return InnerSingleton.INSTANCE;
    }


    /**
     * 测试
     */
    public static void main(String[] args) {
    
    
        for(int i=0;i<100;i++){
    
    
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    Singleton4 temp = getInstance();
                    // 打印出内存地址,如果内存地址不一样,则生成了多个实例
                    System.out.println(temp.toString());
                }
            }).start();
        }
    }
}

C'est également la solution parfaite, recommandée pour les fissures dans les murs.

5. Énumération du mode ultime

Le développement Java sait qu'un livre s'appelle "Effective Java". "Effective Java" a été écrit par Joshua Bloch. Il est le fondateur du Java Collection Framework, qui dirige la conception et la mise en œuvre de nombreuses fonctionnalités de la plate-forme Java, notamment les améliorations du langage JDK 5.0 et le Java Collection Framework primé. Il a quitté SUN en juin 2004 et est devenu l'architecte Java en chef de Google. De plus, il a remporté le prestigieux Jolt Award pour le livre "Effective Java".

Dans ce livre, la solution ultime est donnée, à l'aide d'énumérations.

package com.wuxiaolong.design.pattern.singleton;

/**
 * Description: 
 *  在《Effective Java》这本书中,给出了终极解决方案,使用枚举。
 *  使用枚举这种方式,不仅可以解决线程同步问题,还可以防止反序列化。
 * @author 诸葛小猿
 * @date 2020-08-05
 */
public enum Singleton5 {
    
    

    INSTANCE;

    /**
     * 测试
     */
    public static void main(String[] args) {
    
    
        for(int i=0;i<100;i++){
    
    
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    // 打印出内存地址,如果内存地址不一样,则生成了多个实例
                    System.out.println(Singleton5.INSTANCE.toString());
                }
            }).start();
        }
    }

}

L'utilisation de l'énumération de cette manière peut non seulement résoudre le problème de synchronisation des threads, mais également empêcher la désérialisation.

Pourquoi le mode singleton empêche-t-il la sérialisation et la désérialisation? Dans les formulaires précédents, la classe peut toujours être instanciée sous forme de réflexion (classForName). Étant donné que l'énumération n'a pas de méthode de construction, la classe ne peut pas être instanciée par réflexion.

Bien que grammaticalement, cette façon d'écrire est la plus parfaite, mais de cette façon je ne l'aime pas beaucoup, j'ai l'impression que la définition est la même qu'une constante.

Raisons de l'utilisation de la modification volatile des mots clés

En mode homme affamé, les variables membres INSTANCEsont modifiées avec le mot clé volatile. Quelle est la raison?

S'il n'est pas modifié avec le mot-clé volatile, cela causera un tel problème:

    public static Singleton3 getInstance(){
    
    
        if(INSTANCE == null){
    
    
            synchronized (Singleton3.class){
    
    
                if(INSTANCE == null){
    
    
                    INSTANCE = new Singleton3();
                }
            }
        }
        return INSTANCE;
    }

** Lorsque le thread s'exécute à la ligne 2, il peut arriver que bien que INSTANCE ne soit pas nul, l'objet pointé par INSTANCE n'a pas encore été initialisé. ** La raison principale est la réorganisation. La réorganisation fait référence à un moyen par lequel le compilateur et le processeur réorganisent la séquence d'instructions afin d'optimiser les performances du programme.
La 5ème ligne de code crée un objet, cette ligne de code peut être décomposée en 3 opérations:

memory = allocate();  // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory;  // 3:设置instance指向刚分配的内存地址

La racine est comprise entre 2 et 3 dans le code, qui peut être réorganisé. Par exemple:

memory = allocate();  // 1:分配对象的内存空间
instance = memory;  // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象

Ce n'est pas un problème dans un environnement monothread, mais il y aura des problèmes dans un environnement multithread: une fois les instructions des deuxième et troisième étapes réorganisées, le thread A entre dans le bloc de code synchronisé ci-dessus et exécute INSTANCE = new Singleton3()la ligne In la troisième instruction instance = memory, lorsque le thread B entre INSTANCE != null, il obtiendra un objet qui n'a pas encore été initialisé.

La réorganisation des instructions dans les deuxième et troisième étapes n'affecte pas le résultat final du thread A, mais elle amènera le thread B à déterminer que l'instance n'est pas vide et à accéder à un objet non initialisé.
** Ainsi, seule une petite modification (déclarant l'instance comme volatile) peut réaliser une initialisation retardée thread-safe. ** Parce que la variable modifiée par le mot-clé volatile ne peut pas être réorganisée.

Suivez le compte officiel et entrez " java-summary " pour obtenir le code source.

Terminé, appelez ça un jour!

[ Diffusion des connaissances, partage de la valeur ], merci les amis pour votre attention et votre soutien, je suis [ Zhuge Xiaoyuan ], un travailleur migrant Internet qui lutte dans l'hésitation.

Je suppose que tu aimes

Origine blog.csdn.net/wuxiaolongah/article/details/107851003
conseillé
Classement