Patrón Singleton del patrón de diseño java (Patrón Singleton)

concepto

El patrón singleton ( Singleton Pattern) es uno de los patrones de diseño más simples en Java. Este tipo de patrón de diseño es un patrón de creación. La definición dada en el [libro GOF][3] es: Asegúrese de que una clase tenga solo una instancia y proporcione un punto de acceso global a ella.

El patrón singleton generalmente se refleja en la declaración de clase.La clase singleton es responsable de crear sus propios objetos mientras se asegura de que solo se cree un único objeto. Esta clase proporciona una forma de acceder a su único objeto, directamente, sin instanciar un objeto de esta clase.

usar

El patrón singleton tiene las siguientes dos ventajas:

Solo hay una instancia en la memoria, lo que reduce la sobrecarga de la memoria, especialmente la creación y destrucción frecuente de instancias (como la caché de la página de inicio del sitio web).

Evite ocupaciones múltiples de recursos (como escribir operaciones de archivo).

A veces, cuando elegimos usar el patrón singleton, no solo consideramos las ventajas que trae, sino que también puede tener un patrón singleton en algunos escenarios. Por ejemplo, una situación como "un partido solo puede tener un presidente".

Método para realizar

Sabemos que la generación de un objeto de una clase la realiza el constructor de la clase. Si una clase proporciona publicun constructor al mundo exterior, entonces el mundo exterior puede crear objetos de esta clase arbitrariamente. Por lo tanto, si desea limitar la generación de objetos, una forma es hacer que el constructor sea privado (al menos protegido), de modo que las clases externas no puedan generar objetos por referencia. Al mismo tiempo, para garantizar la disponibilidad de la clase, es necesario proporcionar un objeto propio y un método estático para acceder a este objeto.

[ QQ20160406-0][4]

Estilo chino hambriento

Aquí hay una implementación singleton simple:

//code 1
public class Singleton {
    //在类内部实例化一个实例
    private static Singleton instance = new Singleton();
    //私有的构造函数,外部无法访问
    private Singleton() {
    }
    //对外提供获取实例的静态方法
    public static Singleton getInstance() {
        return instance;
    }
}

Prueba con el siguiente código:

//code2
public class SingletonClient {

    public static void main(String[] args) {
        SimpleSingleton simpleSingleton1 = SimpleSingleton.getInstance();
        SimpleSingleton simpleSingleton2 = SimpleSingleton.getInstance();
        System.out.println(simpleSingleton1==simpleSingleton2);
    }
}

Resultado de salida:

true

El código 1 es una implementación singleton simple, que llamamos el estilo chino hambriento. El llamado hombre hambriento. Esta es una metáfora relativamente vívida. Para un hombre hambriento, espera que cuando quiera usar esta instancia, pueda obtenerla de inmediato sin ningún tiempo de espera. Por lo tanto, a través staticdel método de inicialización estática, cuando la clase se carga por primera vez, SimpleSingletonse crea una instancia de la misma. Esto garantiza que el objeto ya esté inicializado la primera vez que desee utilizarlo.

Al mismo tiempo, dado que la instancia se crea cuando se carga la clase, también se evitan los problemas de seguridad de subprocesos. (Por el motivo, consulte: [Análisis en profundidad del mecanismo ClassLoader de Java (nivel de origen)][5], [Carga, vinculación e inicialización de clases de Java][6])

También hay una variante del modo hombre hambriento:

//code 3
public class Singleton2 {
    //在类内部定义
    private static Singleton2 instance;
    static {
        //实例化该实例
        instance = new Singleton2();
    }
    //私有的构造函数,外部无法访问
    private Singleton2() {
    }
    //对外提供获取实例的静态方法
    public static Singleton2 getInstance() {
        return instance;
    }
}

El código 3 y el código 1 son en realidad iguales, ambos instancian un objeto cuando se carga la clase.

Singleton chino hambriento, se creará una instancia del objeto cuando se cargue la clase. Esto puede causar un consumo innecesario, ya que es posible que esta instancia no se utilice en absoluto. Además, si esta clase se carga varias veces, también provocará múltiples instancias. De hecho, hay muchas maneras de resolver este problema. A continuación se proporcionan dos soluciones. La primera es usar clases internas estáticas. La segunda es usar el estilo perezoso.

clase interna estática

Primero veamos cómo resolver los problemas anteriores a través de clases internas estáticas:

//code 4
public class StaticInnerClassSingleton {
    //在静态内部类中初始化实例对象
    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
    //私有的构造方法
    private StaticInnerClassSingleton() {
    }
    //对外提供获取实例的静态方法
    public static final StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Este método también utiliza el mecanismo de classloder para garantizar que instancesolo haya un subproceso durante la inicialización. Es diferente del estilo chino hambriento (diferencia muy sutil): siempre que se Singletoncargue el estilo chino hambriento, instancese instanciará (no Para lograr el efecto de carga diferida), y de esta manera es que Singletonla clase se carga, instanceno necesariamente se inicializa. Debido a que SingletonHolderla clase no se usa activamente, la clase de carga se muestra y se crea una instancia solo getInstancecuando se llama al método . Imagine que si la creación de instancias consume recursos, quiero que se cargue con pereza. Por otro lado, no quiero crear una instancia cuando se carga la clase, porque no puedo asegurar que la clase se pueda usar activamente en otros lugares. para ser cargado Entonces, la creación de instancias es obviamente inapropiada en este momento. En este momento, este método es más razonable que el hambriento estilo chino.SingletonHolderinstanceinstanceSingletonSingletoninstance

Perezoso

Veamos otro modo singleton que se instancia cuando el objeto se usa realmente: el modo perezoso.

//code 5
public class Singleton {
    //定义实例
    private static Singleton instance;
    //私有构造方法
    private Singleton(){}
    //对外提供获取实例的静态方法
    public static Singleton getInstance() {
        //在对象被使用的时候才实例化
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

El singleton anterior se llama singleton perezoso. La gente perezosa simplemente no crea instancias por adelantado y retrasa la creación de instancias de la clase hasta la primera vez que se hace referencia a ella. getInstanceLa función del método es esperar que el objeto sea liberado cuando se use por primera vez new.

¿Ha notado que en realidad hay un problema con los singletons perezosos como el código 5, y eso es la seguridad de subprocesos? En el caso de subprocesos múltiples, es posible que dos subprocesos ingresen ifla declaración al mismo tiempo, de modo que se crean dos objetos diferentes cuando ambos subprocesos salen del if. (No lo explicaré en detalle aquí, compense el conocimiento de subprocesos múltiples si no lo entiende).

perezoso seguro para subprocesos

Para singletons de estilo perezoso que no son seguros para subprocesos, la solución es realmente muy simple, que es bloquear los pasos de creación de objetos:

//code 6
public class SynchronizedSingleton {
    //定义实例
    private static SynchronizedSingleton instance;
    //私有构造方法
    private SynchronizedSingleton(){}
    //对外提供获取实例的静态方法,对该方法加锁
    public static synchronized SynchronizedSingleton getInstance() {
        //在对象被使用的时候才实例化
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}

Esta forma de escribir puede funcionar bien en multithreading, y parece que también tiene un buen lazy loading, pero, lamentablemente, es muy ineficiente, porque el 99% de los casos no necesita sincronización. synchronized(Debido a que el alcance de bloqueo anterior es el método completo, todas las operaciones de este método se realizan de forma síncrona, pero para el caso de no crear un objeto por primera vez, es decir, el caso de no ingresar la declaración, no hay necesidad de ifoperación síncrona en absoluto, puede regresar directamente instance).

bloqueo de doble verificación

En vista de los problemas existentes en el código 6 anterior, creo que los estudiantes que entienden la programación concurrente saben cómo resolverlos. De hecho, el problema con el código anterior es que el alcance del bloqueo es demasiado grande. Simplemente reduzca el alcance de la cerradura. Entonces, ¿cómo reducir el alcance de la cerradura? Los bloques de código sincronizados tienen un alcance de bloqueo más pequeño que los métodos sincronizados. el código 6 se puede transformar en:

//code 7
public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

El código 7 es una forma mejorada de escribir el código 6, que reduce el alcance de los bloqueos mediante el uso de bloques de código sincronizados. Esto puede mejorar en gran medida la eficiencia. (Para la singletonsituación existente, no hay necesidad de sincronizar, solo regrese directamente).

¿Pero es tan fácil? Parece que el código anterior no tiene nada de malo. Se realiza la inicialización diferida, se resuelve el problema de sincronización, se reduce el alcance del bloqueo y se mejora la eficiencia. Sin embargo, también hay peligros ocultos en este código. Los peligros ocultos están relacionados principalmente con [Java Memory Model (JMM][7]). Considere la siguiente secuencia de eventos:

El subproceso A encuentra que la variable no se ha inicializado, luego adquiere el bloqueo y comienza la inicialización de la variable.

Debido a la semántica de algunos lenguajes de programación, el código generado por el compilador permite que la variable se actualice y apunte a un objeto parcialmente inicializado antes de que el subproceso A haya terminado de realizar la inicialización de la variable.

El subproceso B encuentra que la variable compartida se ha inicializado y devuelve la variable. Dado que el subproceso B está seguro de que la variable se ha inicializado, no adquiere el bloqueo. Si la variable compartida es visible para B antes de que A complete su inicialización (ya sea porque A no ha completado su inicialización o porque algunos valores inicializados aún no han cruzado la memoria utilizada por B (coherencia de caché)), es probable que el programa se bloquee. .

(Estudiantes que no entiendan el ejemplo anterior, por favor complementen el conocimiento sobre el modelo de memoria JAVA)

El uso de bloqueos verificados dos veces en [J2SE 1.4][8] o anterior es potencialmente peligroso y a veces funciona correctamente (distinguir una implementación correcta de una implementación con errores es difícil. Depende del compilador, la programación de subprocesos y otras actividades concurrentes del sistema, los resultados anormales de El bloqueo de verificación doble implementado incorrectamente puede ocurrir de manera intermitente. Reproducir la excepción es muy difícil.) En [J2SE 5.0][8], este problema se solucionó. La palabra clave [volatile][9] garantiza que varios subprocesos puedan manejar correctamente las instancias de singleton

Por tanto, para el código 7, existen dos alternativas, el código 8 y el código 9:

usarvolatile

//code 8
public class VolatileSingleton {
    private static volatile VolatileSingleton singleton;

    private VolatileSingleton() {
    }

    public static VolatileSingleton getSingleton() {
        if (singleton == null) {
            synchronized (VolatileSingleton.class) {
                if (singleton == null) {
                    singleton = new VolatileSingleton();
                }
            }
        }
        return singleton;
    }
}

**El método de bloqueo de doble verificación anterior se usa ampliamente y resuelve todos los problemas mencionados anteriormente. **Sin embargo, incluso este enfoque aparentemente perfecto puede tener problemas, y eso es cuando se trata de la serialización. Los detalles se introducirán más adelante.

usarfinal

//code 9
class FinalWrapper<T> {
    public final T value;

    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class FinalSingleton {
    private FinalWrapper<FinalSingleton> helperWrapper = null;

    public FinalSingleton getHelper() {
        FinalWrapper<FinalSingleton> wrapper = helperWrapper;

        if (wrapper == null) {
            synchronized (this) {
                if (helperWrapper == null) {
                    helperWrapper = new FinalWrapper<FinalSingleton>(new FinalSingleton());
                }
                wrapper = helperWrapper;
            }
        }
        return wrapper.value;
    }
}

Enumeración

Antes de la versión 1.5, generalmente solo existen los métodos anteriores para implementar singletons.Después de 1.5, hay otra forma de implementar singletons, que es usar la enumeración:

// code 10
public enum  Singleton {

    INSTANCE;
    Singleton() {
    }
}

Este método es defendido por el autor de [Effective Java][10] Josh Bloch.No solo puede evitar problemas de sincronización de subprocesos múltiples, sino que también evita que la deserialización recree nuevos objetos (descrito a continuación), lo que puede describirse como una barrera muy fuerte. , En el análisis en profundidad de los tipos de enumeración de Java --- problemas de serialización y seguridad de subprocesos de enumeración, hay introducciones detalladas a los problemas de seguridad de subprocesos de enumeración y problemas de serialización. Sin embargo, personalmente creo que la característica solo se agregó en 1.5 enum. El método de escritura inevitablemente hace que las personas se sientan desconocidas. En el trabajo real, rara vez veo a alguien escribiendo así, pero eso no significa que no sea bueno.

Singletons y serialización

En el artículo [Cosas sobre singletons y serialización][11], [Hollis][12] analizó la relación entre singletons y serialización antes de que la serialización pueda destruir singletons. Para evitar que la serialización destruya el singleton, simplemente Singletondefínalo en la clase readResolvepara resolver el problema:

//code 11
package com.hollis;
import java.io.Serializable;
/**
 * Created by hollis on 16/2/5.
 * 使用双重校验锁方式实现单例
 */
public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    private Object readResolve() {
        return singleton;
    }
}

Resumir

Este artículo presenta varios métodos para realizar singleton, incluidos principalmente el hombre hambriento, el hombre perezoso, el uso de una clase interna estática, el bloqueo de doble verificación, la enumeración, etc. También cubre cómo evitar que la serialización rompa el singleton de una clase.

A partir de la implementación de singleton, podemos encontrar que un patrón de singleton simple puede implicar mucho conocimiento. En el proceso de mejora continua, se pueden comprender y aplicar más conocimientos. El llamado aprendizaje es interminable.

Supongo que te gusta

Origin blog.csdn.net/zy_dreamer/article/details/132364359
Recomendado
Clasificación