Pesquisa de site

Melhores práticas de padrão de design Java Singleton com exemplos


Introdução

Java Singleton Pattern é um dos Gangs of Four Design patterns e vem na categoria Creational Design Pattern. Pela definição, parece ser um padrão de projeto simples, mas quando se trata de implementação, vem com muitas preocupações.

Neste artigo, aprenderemos sobre os princípios do padrão de projeto singleton, exploraremos diferentes maneiras de implementar o padrão de projeto singleton e algumas das melhores práticas para seu uso.

Princípios do Padrão Singleton

  • O padrão Singleton restringe a instanciação de uma classe e garante que apenas uma instância da classe exista na Java Virtual Machine.
  • A classe singleton deve fornecer um ponto de acesso global para obter a instância da classe.
  • O padrão singleton é usado para pool de threads.
  • O padrão de design singleton também é usado em outros padrões de design, como fachada, etc.
  • O padrão de design singleton também é usado nas principais classes Java (por exemplo, java.lang.Runtime, java.awt.Desktop).

Implementação de padrão Java Singleton

Para implementar um padrão singleton, temos diferentes abordagens, mas todas elas possuem os seguintes conceitos comuns.

  • Construtor privado para restringir a instanciação da classe de outras classes.
  • Variável estática privada da mesma classe que é a única instância da classe.
  • Método estático público que retorna a instância da classe, este é o ponto de acesso global para o mundo externo obter a instância da classe singleton.

Em seções posteriores, aprenderemos diferentes abordagens para implementação de padrão singleton e preocupações de design com a implementação.

1. Inicialização antecipada

Na inicialização antecipada, a instância da classe singleton é criada no momento do carregamento da classe. A desvantagem da inicialização antecipada é que o método é criado mesmo que o aplicativo cliente não o esteja usando. Aqui está a implementação da classe singleton de inicialização estática:

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // private constructor to avoid client applications using the constructor
    private EagerInitializedSingleton(){}

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

Se sua classe singleton não estiver usando muitos recursos, esta é a abordagem a ser usada. Mas na maioria dos cenários, classes singleton são criadas para recursos como sistema de arquivos, conexões de banco de dados, etc. Devemos evitar a instanciação, a menos que o cliente chame o método getInstance. Além disso, esse método não fornece nenhuma opção para tratamento de exceção.

2. Inicialização do bloco estático

manipulação de exceção.

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // static block initialization for exception handling
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

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

Tanto a inicialização antecipada quanto a inicialização de bloco estático criam a instância antes mesmo de ela ser usada e essa não é a melhor prática a ser usada.

3. Inicialização lenta

O método de inicialização preguiçoso para implementar o padrão singleton cria a instância no método de acesso global. Aqui está o código de exemplo para criar a classe singleton com esta abordagem:

package com.journaldev.singleton;

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

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

A implementação anterior funciona bem no caso do ambiente de encadeamento único, mas quando se trata de sistemas multiencadeados, pode causar problemas se vários encadeamentos estiverem dentro da condição if ao mesmo tempo. Isso destruirá o padrão singleton e ambos os threads obterão instâncias diferentes da classe singleton. Na próxima seção, veremos maneiras diferentes de criar uma classe singleton thread-safe.

4. Singleton Thread Safe

Uma maneira simples de criar uma classe singleton thread-safe é tornar o método de acesso global sincronizado para que apenas um thread possa executar esse método por vez. Aqui está uma implementação geral dessa abordagem:

package com.journaldev.singleton;

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }

}

A implementação anterior funciona bem e oferece segurança de encadeamento, mas reduz o desempenho devido ao custo associado ao método sincronizado, embora seja necessário apenas para os primeiros encadeamentos que podem criar instâncias separadas. Para evitar essa sobrecarga extra todas as vezes, o princípio de bloqueio verificado duas vezes é usado. Nesta abordagem, o bloco sincronizado é usado dentro da condição if com uma verificação adicional para garantir que apenas uma instância de uma classe singleton seja criada. O trecho de código a seguir fornece a implementação de bloqueio verificado duas vezes:

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

Continue seu aprendizado com Thread Safe Singleton Class.

5. Implementação de Bill Pugh Singleton

Antes do Java 5, o modelo de memória Java tinha muitos problemas e as abordagens anteriores costumavam falhar em determinados cenários em que muitos encadeamentos tentavam obter a instância da classe singleton simultaneamente. Portanto, classe auxiliar estática interna. Aqui está um exemplo da implementação de Bill Pugh Singleton:

package com.journaldev.singleton;

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

Observe a classe estática interna privada que contém a instância da classe singleton. Quando a classe singleton é carregada, a classe SingletonHelper não é carregada na memória e somente quando alguém chama o método getInstance(), esta classe é carregada e cria a instância da classe singleton. Essa é a abordagem mais usada para a classe singleton, pois não requer sincronização.

6. Usando a Reflexão para destruir o Padrão Singleton

A reflexão pode ser usada para destruir todas as abordagens de implementação singleton anteriores. Aqui está uma classe de exemplo:

package com.journaldev.singleton;

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) {
                // This 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());
    }

}

Ao executar a classe de teste anterior, você notará que hashCode de ambas as instâncias não é o mesmo, o que destrói o padrão singleton. A reflexão é muito poderosa e usada em muitos frameworks como Spring e Hibernate. Continue seu aprendizado com o Java Reflection Tutorial.

7. Enum Singleton

Para superar essa situação com o Reflection, os valores Java Enum são globalmente acessíveis, assim como o singleton. A desvantagem é que o tipo enum é um tanto inflexível (por exemplo, não permite inicialização preguiçosa).

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

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

8. Serialização e Singleton

Às vezes, em sistemas distribuídos, precisamos implementar a interface Serializable na classe singleton para que possamos armazenar seu estado no sistema de arquivos e recuperá-lo posteriormente. Aqui está uma pequena classe singleton que também implementa a interface Serializable:

package com.journaldev.singleton;

import java.io.Serializable;

public class SerializedSingleton implements Serializable {

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}

    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }

}

O problema com a classe singleton serializada é que sempre que a desserializarmos, ela criará uma nova instância da classe. Aqui está um exemplo:

package com.journaldev.singleton;

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();

        // deserialize 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());

    }

}

Esse código produz esta saída:

Output
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522

Portanto, destrói o padrão singleton. Para superar esse cenário, tudo o que precisamos fazer é fornecer a implementação do método readResolve().

protected Object readResolve() {
    return getInstance();
}

Depois disso, você notará que o hashCode de ambas as instâncias é o mesmo no programa de teste.

Leia sobre a desserialização Java.

Conclusão

Este artigo abordou o padrão de design singleton.

Continue seu aprendizado com mais tutoriais de Java.

Artigos relacionados: