Introdução
O Padrão Singleton Java é um dos padrões de design do Gangue dos Quatro e pertence à categoria de Padrões de Design Criacional. Pela definição, parece ser um padrão de design direto, mas quando se trata de implementação, vem com muitas preocupações.
Neste artigo, aprenderemos sobre os princípios do padrão de design singleton, exploraremos diferentes maneiras de implementar o padrão de design singleton e algumas das melhores práticas para seu uso.
Princípios do Padrão Singleton
- O padrão singleton restringe a instância de uma classe e garante que apenas uma instância da classe exista na Máquina Virtual Java.
- A classe singleton deve fornecer um ponto de acesso global para obter a instância da classe.
- O padrão singleton é usado para registro, objetos de driver, armazenamento em cache e pool de threads.
- O padrão de design Singleton também é usado em outros padrões de design como Abstract Factory, Builder, Prototype, Facade, etc.
- O padrão de design Singleton também é usado em classes principais do Java (por exemplo,
java.lang.Runtime
,java.awt.Desktop
).
Implementação do padrão Singleton em Java
Para implementar um padrão singleton, temos diferentes abordagens, mas todas elas têm os seguintes conceitos em comum.
- Construtor privado para restringir a instanciação da classe a partir 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 a implementação do padrão singleton e preocupações de design com a implementação.
1. Inicialização ansiosa
Na inicialização ansiosa, a instância da classe singleton é criada no momento da carga da classe. A desvantagem da inicialização ansiosa é que o método é criado mesmo que a aplicação cliente possa não estar usando-o. Aqui está a implementação da classe singleton com inicialização estática:
package com.journaldev.singleton;
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
// construtor privado para evitar que as aplicações clientes usem o construtor
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance() {
return instance;
}
}
Se a sua classe singleton não estiver usando muitos recursos, este é o método a ser utilizado. Mas na maioria dos cenários, as classes singleton são criadas para recursos como Sistema de Arquivos, conexões de Banco de Dados, etc. Devemos evitar a instância a menos que o cliente chame o método getInstance
. Além disso, este método não fornece opções para tratamento de exceções.
2. Inicialização por bloco estático
A implementação de inicialização de bloco estático é semelhante à inicialização ansiosa, exceto que a instância da classe é criada no bloco estático que fornece a opção para manipulação de exceções.
package com.journaldev.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
// inicialização de bloco estático para manipulação de exceções
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 ansiosa quanto a inicialização de bloco estático criam a instância mesmo antes de ser utilizada e isso não é a melhor prática a ser seguida.
3. Inicialização Preguiçosa
O método de inicialização preguiçosa 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 essa 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 de ambiente de thread único, mas quando se trata de sistemas com múltiplas threads, pode causar problemas se várias threads estiverem dentro da if
condição ao mesmo tempo. Isso irá destruir o padrão singleton e ambas as threads obterão instâncias diferentes da classe singleton. Na próxima seção, veremos diferentes maneiras de criar uma classe singleton segura para threads.
4. Singleton Seguro para Threads
A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:
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 fornece segurança para threads, mas reduz o desempenho devido ao custo associado ao método sincronizado, embora só o precisemos para as primeiras threads que podem criar instâncias separadas. Para evitar essa sobrecarga extra toda vez, o princípio do bloqueio de dupla verificação é utilizado. 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 do bloqueio de dupla verificação:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
Continue aprendendo com Classe Singleton Segura para Threads.
5. Implementação Singleton de Bill Pugh
Antes do Java 5, o modelo de memória do Java tinha muitos problemas, e as abordagens anteriores costumavam falhar em cenários onde muitas threads tentavam obter a instância da classe singleton simultaneamente. Então, Bill Pugh propôs uma abordagem diferente para criar a classe singleton usando uma classe auxiliar estática interna. Aqui está um exemplo da implementação do Singleton de Bill Pugh:
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 interna estática 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. Esta é a abordagem mais amplamente utilizada para a classe singleton, pois não requer sincronização.
6. Usando Reflexão para destruir o Padrão Singleton
A reflexão pode ser usada para destruir todas as abordagens de implementação de singleton anteriores. Aqui está um exemplo de classe:
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) {
// Este código irá destruir o padrão singleton
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
Quando você executar a classe de teste anterior, você notará que o 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 sua aprendizagem com o Tutorial de Reflexão em Java.
7. Singleton Enum
Para superar essa situação com Reflexão, Joshua Bloch sugere o uso de enum
para implementar o padrão de design singleton, pois o Java garante que qualquer valor de enum
seja instanciado apenas uma vez em um programa Java. Como os valores de Enum Java são globalmente acessíveis, também é o singleton. A desvantagem é que o tipo enum
é um pouco inflexível (por exemplo, não permite inicialização preguiçosa).
package com.journaldev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// faça algo
}
}
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 deserializamos, 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();
// deserializar do arquivo para objeto
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 a seguinte saída:
OutputinstanceOne hashCode=2011117821
instanceTwo hashCode=109647522
Assim, 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 Serialização Java e a Deserialização Java.
Conclusão
Este artigo abordou o padrão de design singleton.
Continue aprendendo com mais tutoriais Java.