Introdução
O Padrão Singleton em Java é um dos Padrões de Projeto Gangue dos Quatro e faz parte da categoria de Padrões de Projeto Criacionais. Pela definição, parece ser um padrão de design simples, mas quando se trata de implementação, surgem muitas preocupações.
Neste artigo, aprenderemos sobre os princípios do padrão de design singleton, exploraremos diferentes formas 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 instanciação 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, cache e pool de threads.
- O padrão de design Singleton também é utilizado em outros padrões de design como o Abstract Factory, Builder, Prototype, Facade, etc.
- O padrão de design Singleton é utilizado em classes essenciais do Java também (por exemplo,
java.lang.Runtime
,java.awt.Desktop
).
Implementação do Padrão Singleton em Java
Para implementar um padrão singleton, temos abordagens diferentes, mas todas elas possuem os seguintes conceitos comuns.
- Construtor privado para restringir a instanciação da classe por 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 exterior obter a instância da classe singleton.
Nas seções seguintes, aprenderemos diferentes abordagens para a implementação do padrão singleton e preocupações de design associadas à implementação.
1. Inicialização ansiosa
Na inicialização ansiosa, a instância da classe singleton é criada no momento do carregamento 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 de 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 cliente usem o construtor
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, este método não oferece nenhuma opção para tratamento de exceção.
2. Inicialização por bloco estático
Bloco estático A implementação da inicialização é semelhante à inicialização precoce, exceto que a instância da classe é criada no bloco estático, proporcionando a opção para o tratamento de exceções.
package com.journaldev.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
// Inicialização do bloco estático para tratamento 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 precoce quanto a inicialização do bloco estático criam a instância mesmo antes de ser utilizada, o que não é a melhor prática de uso.
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 um ambiente de thread único, mas quando se trata de sistemas multi-threaded, pode causar problemas se vários threads 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 diferentes maneiras de criar uma classe singleton segura para threads.
4. Singleton Thread-Safe
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 oferece segurança de thread, mas reduz o desempenho devido ao custo associado ao método sincronizado, embora só precisemos disso para as primeiras threads que podem criar instâncias separadas. Para evitar essa sobrecarga extra toda vez, é utilizado o princípio de bloqueio de verificação dupla. Nessa 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 verificação dupla:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
Continue aprendendo com Classe Singleton Thread-Safe.
5. Implementação Singleton de Bill Pugh
Antes do Java 5, o modelo de memória do Java apresentava muitos problemas, e as abordagens anteriores costumavam falhar em cenários específicos nos quais 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 interna estática. Aqui está um exemplo da implementação do Singleton por 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 privada e estática 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()
, essa classe é carregada e cria a instância da classe singleton. Essa é 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 anteriores de implementação do singleton. 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 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 aprendendo com o Tutorial de Reflexão em Java.
7. Singleton Enum
Para superar essa situação com a Reflexão, Joshua Bloch sugere o uso de enum
para implementar o padrão de design singleton, já que o Java garante que qualquer valor de enum
seja instanciado apenas uma vez em um programa Java. Como os valores de Enum em Java são globalmente acessíveis, o singleton também é. A desvantagem é que o tipo enum
é um pouco inflexível (por exemplo, não permite inicialização tardia).
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 desserializamos, 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();
// desserializar 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 esta 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 Serialização Java e Desserialização Java.
Conclusão
Este artigo cobriu o padrão de design singleton.
Continue sua aprendizagem com mais tutoriais Java.