Um artigo para obter o modo singleton

1 Conceitos básicos

1.1 Qual é o padrão singleton

O padrão singleton é um padrão de design criativo que permite garantir que haja apenas uma instância de uma classe e fornecer um método global para acessar essa instância.

Modo de criação: este tipo de modo fornece um mecanismo para a criação de objetos, que pode melhorar a flexibilidade e a capacidade de reutilização do código existente.

1.2 Por que usar o modo singleton

  • Resolva o problema de conflitos de recursos
    Para alguns recursos globais, por exemplo, temos um programa que usa uma impressora (há apenas uma no projeto). Haverá várias solicitações para usar a impressora, mas o recurso de impressora não pode ser criado repetidamente .
  • Classe globalmente exclusiva
    Alguns dados devem ser mantidos apenas no sistema. Por exemplo, informações de configuração, o arquivo de configuração do sistema deve ter apenas uma cópia, que existe como um objeto após ser carregado na memória para proteger a instância de ser sobrescrita por outros códigos.
    Algumas classes são frequentemente chamadas por códigos em vários lugares, por exemplo, a classe de conexão de banco de dados é definida pelo método getInstance (get instance) para que o cliente possa acessar a mesma instância de conexão de banco de dados em qualquer lugar do programa.

Cenários de aplicação comuns:

  • exploração madeireira
  • objetos motoristas
  • caching
  • Grupo de discussão
  • java.lang.Runtime / java.awt.Desktop
  • O ciclo de vida padrão do Bean na primavera

1.3 Solução básica

A realização do padrão singleton inclui as duas etapas básicas a seguir:

  • Torne o construtor privado para evitar que outros objetos usem o novo operador da classe singleton, ou seja, ele não pode ser instanciado fora da classe singleton, mas só pode ser instanciado dentro da classe singleton
  • Para criar uma instância estática privada desta classe nesta classe, você deve criar a instância exclusiva você mesmo
  • Fornece uma função global para acessar a instância, este método irá chamar o construtor privado singleton para criar o objeto e salvá-lo em uma variável de membro estático. Todas as chamadas subsequentes para esta função retornarão este objeto de cache.

Se o seu código pode acessar a classe singleton, ele pode chamar o método estático da classe singleton. Sempre que este método for chamado, ele sempre retornará o mesmo objeto.

O diagrama de padrão de estrutura é o seguinte:
Insira a descrição da imagem aqui
Singleton precisa considerar as seguintes questões :

  • Há problemas de segurança de thread quando o multithreading é criado?
  • Suporte para carregamento lento
  • O desempenho de getInstance () é alto?

1.4 Vantagens e desvantagens

vantagem:

  • Pode ser garantido que há apenas uma instância de uma classe
  • Obteve um nó de acesso global apontando para a instância, que pode otimizar o acesso a recursos compartilhados
  • O objeto singleton é inicializado apenas quando é solicitado pela primeira vez para evitar a criação e destruição frequente do objeto, o que pode melhorar o desempenho

Desvantagens:

  • O padrão singleton pode ocultar um projeto ruim, como um entendimento excessivo entre os componentes do programa.
  • O teste de unidade de código do cliente singleton pode ser mais difícil, porque muitas estruturas de teste criam objetos fictícios com base na herança. Como o construtor de uma classe de singleton é privado e a maioria das linguagens não pode substituir métodos estáticos, você precisa pensar cuidadosamente sobre como simular um singleton. Não escreva código de teste ou não use o modo singleton.
  • O modo singleton requer processamento especial em um ambiente multithread para evitar que vários threads criem objetos singleton várias vezes.

2 Implementação típica

2.1 Variável Estática Chinesa Hungry

A característica do faminto estilo chinês é criá-lo diretamente, quer seja necessário agora ou não, crie-o primeiro.
Quando a classe é carregada, ela é instanciada diretamente na variável estática.

public class EagerInitializedSingleton {
    
    
    
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
    
    private EagerInitializedSingleton(){
    
    }

    public static EagerInitializedSingleton getInstance(){
    
    
        return instance;
    }
}

vantagem

  • getInstance () tem bom desempenho, segurança de thread e implementação simples
  • Devido ao uso da palavra-chave estática, é garantido que, quando esta variável é referenciada, todas as operações de gravação nesta variável são concluídas, então a segurança do encadeamento no nível da JVM é garantida

Desvantagem

  • Se uma classe ocupa muitos recursos de memória, carregamos esta classe durante a inicialização, mas não usamos esta classe há muito tempo, resultando em um desperdício de espaço de memória

2.2 Bloco de código estático chinês Hungry

A diferença entre este e 2.1 é que ele é instanciado em um bloco de código estático, mas colocar novo no bloco de código estático tem outras vantagens, ou seja, você pode fazer algumas outras operações, como inicializar algumas variáveis, ler alguns dados do arquivo de configuração, etc.

public class StaticBlockSingleton {
    
    

    private static StaticBlockSingleton instance;
    
    private StaticBlockSingleton(){
    
    }
    
    static{
    
    
        try{
    
    
            // do something else
            instance = new StaticBlockSingleton();
        }catch(Exception e){
    
    
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    
    public static StaticBlockSingleton getInstance(){
    
    
        return instance;
    }
}

2.3 Thread único preguiçoso

Instancie quando necessário

public class LazyInitializedSingleton {
    
    

    private static LazyInitializedSingleton instance;
    
    private LazyInitializedSingleton(){
    
    }
    
    public static LazyInitializedSingleton getInstance(){
    
    
        if(instance == null){
    
    
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

A implementação acima só se aplica a cenários de thread único.Em situações de multi-thread, podem ocorrer problemas de segurança de thread, levando à criação de diferentes instâncias. Se vários encadeamentos chamarem getInstance () ao mesmo tempo, haverá problemas de simultaneidade. Vários encadeamentos podem obter o julgamento de instance == null ao mesmo tempo, então eles serão instanciados repetidamente, e um singleton não é um singleton. Veja abaixo a solução

2.4 Lazy-synchronized

Ao usar synchronized para modificar o método getInstance () para garantir acesso síncrono ao método, ele se torna um sistema serial estrito, mas o desempenho será relativamente ruim, mesmo se forem alguns threads que solicitam uma classe singleton pela primeira vez.

public class ThreadSafeSingleton {
    
    

    private static ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton(){
    
    }
    
    //synchronized修饰函数
    public static synchronized ThreadSafeSingleton getInstance(){
    
    
        if(instance == null){
    
    
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}

2.5 Lazy-Double check-volatile

Este método melhora o desempenho do acesso estreitando o intervalo de sincronização, e o bloco de código de sincronização controla a criação simultânea de instâncias. Inspeção dupla (dois espaços internos e externos)

public class ThreadSafeSingleton {
    
    

    private static volatile ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton(){
    
    }
    
    public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
    
    
    	if(instance == null){
    
    
        	synchronized (ThreadSafeSingleton.class) {
    
    
            	if(instance == null){
    
    
                	instance = new ThreadSafeSingleton();
            	}
        	}
    	}
    	return instance;
	}
}

O papel do espaço interno:

  • Quando dois threads executam o primeiro nulo ao mesmo tempo, se ambos estiverem satisfeitos, eles entrarão e competirão pelo bloqueio. Supondo que o thread 1 obtenha o bloqueio, executa o conteúdo do bloco de código sincronizado, cria uma instância e retorna, e libera o bloqueio. Em seguida, o thread 2 obtém o bloqueio e executa o código no bloco de código de sincronização. Como o thread 1 foi criado neste momento, embora o thread 2 tenha obtido o bloqueio, se a verificação interna não for adicionada, o thread 2 ser novo novamente, resultando em dois O controle thread-safe está realmente funcionando internamente.

Você pode apenas adicionar a camada interna e torná-la ok

O papel do espaço sideral;

  • O apagamento interno já pode atender à segurança da rosca, e a finalidade de adicionar o apagamento externo é melhorar a eficiência.
  • Porque pode haver tal situação: se a camada externa não for adicionada, o thread 1 executa o bloco de código de sincronização após obter o bloqueio. Depois de novo, quando o bloqueio não foi liberado, o thread 2 vem e está aguardando o bloqueio (neste momento, o encadeamento 1 criou a instância, mas o encadeamento 2 ainda não liberou o bloqueio), depois que o encadeamento 1 libera o bloqueio, o encadeamento 2 obtém o bloqueio, entra no bloco de código de sincronização e retorna para a instância diretamente.
  • Nesse caso, o thread 2 não precisa mais esperar pelo bloqueio? Porque o thread 1 criou a instância, mas não liberou o bloqueio.
  • Portanto, um julgamento vazio é adicionado à camada externa para evitar essa situação. Após a passagem do fio 2, ele é considerado vazio. Se não estiver vazio, não há necessidade de esperar pelo bloqueio, o que melhora a eficiência.

Papel volátil:

  • No caso de multithreading, o modo de bloqueio de verificação dupla pode ter um problema de ponteiro nulo. A razão para o problema é que a JVM executará operações de otimização e reordenação de instruções ao instanciar o objeto, porque a nova linha de código não é uma instrução atômica . Será dividido em várias instruções.

A instanciação de um objeto pode ser dividida nas seguintes 4 etapas:
1. Alocar espaço de memória para o objeto
2. Inicializar o valor padrão (diferente da inicialização do método construtor)
3. Executar o método construtor
Aponte o objeto para o novo
compilador de espaço de memória alocado Ou o processador pode reordenar as etapas 3 e 4 por motivos de desempenho:
1. Alocar espaço de memória para o objeto
2. Inicializar o valor padrão
3. Aponte o objeto para o espaço de memória recém-alocado
4. Execute o método do construtor
O thread pode obter um objeto que não foi inicializado

2.6 Classe interna

Este método é seguro para threads, adequado para multithreading e tira vantagem das características das classes internas java: classes internas estáticas não serão inicializadas automaticamente com o carregamento e inicialização de classes externas, e classes internas devem ser carregadas e inicializadas separadamente. Dessa forma, o objeto singleton é criado quando a classe interna é carregada e inicializada, portanto, é thread-safe e implementa inicialização lenta.

public class BillPughSingleton {
    
    

    private BillPughSingleton(){
    
    }
    
    private static class SingletonHelper{
    
    
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }
    
    public static BillPughSingleton getInstance(){
    
    
        return SingletonHelper.INSTANCE;
    }
}

2.7 Enumeração

A enumeração é a mais concisa, não há necessidade de considerar a privatização do construtor. É importante notar que a classe de enumeração não pode ser herdada, porque o padrão da classe de enumeração é a classe final após a compilação, o que pode evitar que seja modificada por subclasses. A classe constante pode ser herdada, modificada, campos adicionados, etc., o que pode facilmente levar à incompatibilidade da classe pai. O tipo de enumeração é thread-safe e só será carregado uma vez. O designer faz uso total desse recurso da enumeração para implementar o modo singleton. A enumeração é muito simples de escrever e o tipo de enumeração é a única implementação singleton usada. Um modo de implementação singleton que não será destruído.
Vantagens: getInstance () tem alto desempenho de acesso e segurança de thread.
Desvantagens: inicialização não atrasada

public enum EnumSingleton {
    
    

    INSTANCE;
    
    public static void doSomething(){
    
    
        //do something
    }
}

public class EnumSingletonEnumTest {
    
    
    public static void main(String[] args) {
    
    
        EnumSingletonEnum instance = EnumSingletonEnum.INSTANCE;
        System.out.println(instance);
        instance.doSomething();
    }
}

2.8 Resumo

método vantagem Desvantagem
Variáveis ​​estáticas chinesas famintas Segurança de rosca, alto desempenho de acesso Não é possível atrasar a inicialização
Bloco de código estático chinês Hungry Segurança de rosca, alto desempenho de acesso, suporte para operações adicionais Não é possível atrasar a inicialização
Única discussão preguiçosa Alto desempenho de acesso, inicialização atrasada Não é seguro para discussão
Preguiçoso + sincronizado Thread segura, inicialização atrasada O desempenho não é alto
Homem preguiçoso + verificação dupla + volátil Segurança de thread, inicialização atrasada, alto desempenho de acesso -
Classe interna Segurança de thread, inicialização atrasada, alto desempenho de acesso -
enumerar Segurança de rosca, alto desempenho de acesso, segurança Não é possível atrasar a inicialização

3 Métodos e medidas preventivas para destruir o modo singleton

Exceto pela enumeração, há dois problemas na implementação do modo singleton: São esses dois problemas que fazem com que o modo singleton seja destruído se nenhuma medida for tomada.

3.1 Destruição do modo singleton através da reflexão

Reflexão é gerar novos objetos chamando à força métodos de construção privados.

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {
    
    

    public static void main(String[] args) {
    
    
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
    
    
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
    
    
                //Below code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }
}

Método de prevenção

Se queremos evitar a destruição de um singleton, podemos julgar no método de construção, se houver uma instância, podemos evitar que uma nova instância seja gerada.

private Singleton(){
    
    
    if (instance != null){
    
    
        throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
    }
}

3.2 Serialização e modo singleton

Às vezes, em um sistema distribuído, precisamos implementar uma interface serializável em uma classe singleton, para que possamos armazenar seu estado no sistema de arquivos e recuperá-lo posteriormente. A seguir está uma demonstração singleton que implementa uma interface serializável:

import java.io.Serializable;

public class SerializedSingleton implements Serializable{
    
    

    private static final long serialVersionUID = -3423566758394737115L;

    private SerializedSingleton(){
    
    }
    
    private static class SingletonHelper{
    
    
        private static final SerializedSingleton instance = new SerializedSingleton();
    }
    
    public static SerializedSingleton getInstance(){
    
    
        return SingletonHelper.instance;
    }   
}

O problema de serializar uma classe singleton é que sempre que ela é desserializada, ele cria uma nova instância da classe. Os exemplos são os seguintes:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SingletonSerializedTest {
    
    

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
    
    
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();
        
        //deserailize from file to object
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();
        
        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
        
    }
}

A saída é a seguinte, você pode ver que o hashcode é diferente, a classe singleton foi destruída com sucesso e um novo objeto singleton foi criado por new.

instanceOne hashCode=2011117821
instanceTwo hashCode=109647522

Método de prevenção

  1. Não implementa interface de serialização
  2. Se você deve implementar a interface de serialização, você pode substituir o método de desserialização readResolve () para retornar diretamente o objeto singleton relevante durante a desserialização.
protected Object readResolve() {
    
    
    return getInstance();
}

3.3 Destruição da interface clonável

Semelhante à interface serializável, quando a classe que precisa implementar um singleton permite clone (), se não for tratada corretamente, também fará com que mais de uma instância apareça no programa.

public class Singleton implements Cloneable{
    
    
    private static volatile Singleton mInstance;
    private Singleton(){
    
    
    }
    public static Singleton getInstance(){
    
    
        if(mInstance == null){
    
    
            synchronized (Singleton.class) {
    
    
                if(mInstance == null){
    
    
                    mInstance = new Singleton();
                }
            }
        }
        return mInstance;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
    
    
        // TODO Auto-generated method stub
        return super.clone();
    }
}

public class SingletonDemo {
    
    

    public static void main(String[] args){
    
    
        try {
    
    
            Singleton singleton = Singleton.getInstance();
            Singleton cloneSingleton;
            cloneSingleton = (Singleton) Singleton.getInstance().clone();
            System.out.println(cloneSingleton == singleton);
        } catch (CloneNotSupportedException e) {
    
    
            e.printStackTrace();
        }
    }
}

O resultado é falso

Método de prevenção

Substitua o método clone () e retorne diretamente o objeto já instanciado ao chamar clone ().

protected Object clone() throws CloneNotSupportedException {
    
    
        return mInstance;
}

Acho que você gosta

Origin blog.csdn.net/qq_32505207/article/details/109328222
Recomendado
Clasificación