O padrão de projeto Singleton é um dos padrões criacionais mais conhecidos e utilizados, cujo propósito é garantir que uma classe tenha apenas uma instância e fornecer um ponto de acesso global a essa instância. Isso é particularmente útil para recursos que devem ser únicos dentro de um sistema, como gerenciadores de configuração, pools de conexões de banco de dados ou objetos de log.
- Inicialização Ansiosa (Eager Initialization)
Esta é a forma mais simples de implementar o Singleton. A instância é criada no momento em que a classe é carregada pelo ClassLoader, garantindo que ela esteja pronta para uso assim que for requisitada. Embora seja intrinsecamente thread-safe, a desvantagem é que a instância é criada mesmo que o aplicativo nunca a utilize, consumindo recursos desnecessariamente.
package com.example.designpatterns;
public class GerenciadorConexao {
// A instância única é criada quando a classe é carregada.
private static final GerenciadorConexao INSTANCIA_UNICA = new GerenciadorConexao();
// Construtor privado para evitar instanciação externa.
private GerenciadorConexao() {
System.out.println("GerenciadorConexao instanciado.");
}
// Método estático público para acessar a instância única.
public static GerenciadorConexao obterInstancia() {
return INSTANCIA_UNICA;
}
public void conectar() {
System.out.println("Conectando ao banco de dados...");
}
}
- Inicialização Preguiçosa (Lazy Initialization)
Ao contrário da inicialização ansiosa, a inicialização preguiçosa adia a criação da instância até que ela seja realmente necessária. Isso economiza recursos, mas introduz um desafio: a segurança de threads em ambientes concorrentes.
Versão Não Sincronizada (Não Thread-Safe)
Na sua forma mais básica, sem sincronização, esta implementação pode falhar em um ambiente multi-threaded, onde múltiplas threads podem tentar criar a instância simultaneamente, resultando em mais de uma instância.
package com.example.designpatterns;
public class ConfiguradorSistema {
private static ConfiguradorSistema instancia = null;
private ConfiguradorSistema() {
System.out.println("ConfiguradorSistema instanciado.");
}
public static ConfiguradorSistema getInstancia() {
// NÃO THREAD-SAFE: Múltiplas threads podem entrar aqui e criar várias instâncias.
if (instancia == null) {
instancia = new ConfiguradorSistema();
}
return instancia;
}
public void carregarConfiguracoes() {
System.out.println("Carregando configurações do sistema...");
}
}
Versão Sincronizada (Thread-Safe, mas Lenta)
Para resolver o problema de segurança de threads, podemos sincronizar o método de acesso. No entanto, sincronizar o método inteiro impõe um bloqueio em cada chamada, o que pode degradar significativamente o desempenho em aplicações de alta concorrência, pois apenas uma thread pode acessar o método por vez, mesmo após a instância já ter sido criada.
package com.example.designpatterns;
public class ConfiguradorSistemaOtimizado {
private static ConfiguradorSistemaOtimizado instancia = null;
private ConfiguradorSistemaOtimizado() {
System.out.println("ConfiguradorSistemaOtimizado instanciado.");
}
// THREAD-SAFE: O método é sincronizado, mas isso afeta o desempenho.
public static synchronized ConfiguradorSistemaOtimizado getInstanciaSegura() {
if (instancia == null) {
instancia = new ConfiguradorSistemaOtimizado();
}
return instancia;
}
public void recarregarConfiguracoes() {
System.out.println("Recarregando configurações do sistema com sincronização...");
}
}
- Bloqueio de Verificação Dupla (Double-Checked Locking - DCL)
O DCL busca combinar os benefícios da inicialização preguiçosa com a segurança de threads, minimizando o impacto no desempenho. Ele utiliza um bloqueio apenas na primeira vez que a instância é criada. O uso da palavra-chave volatile é crucial para garantir que as operações de leitura e escrita na variável instancia sejam atômicas e visíveis em todas as threads, prevenindo problemas de reordenação de instruções pela JVM.
package com.example.designpatterns;
public class ServicoRegistro {
// 'volatile' garante visibilidade e previne reordenação de instruções.
private static volatile ServicoRegistro instanciaRegistro = null;
private ServicoRegistro() {
System.out.println("ServicoRegistro instanciado.");
}
public static ServicoRegistro obterServico() {
// Primeira verificação (sem bloqueio)
if (instanciaRegistro == null) {
// Bloqueia apenas se a instância ainda não foi criada.
synchronized (ServicoRegistro.class) {
// Segunda verificação (dentro do bloqueio)
if (instanciaRegistro == null) {
instanciaRegistro = new ServicoRegistro();
}
}
}
return instanciaRegistro;
}
public void registrarEvento(String mensagem) {
System.out.println("Log: " + mensagem);
}
}
- Classe Interna Estática (Static Inner Class)
Esta abordagem é considerada uma das melhores para implementar o Singleton em Java. Ela combina a inicialização preguiçosa com a segurança de threads sem a necessidade de bloqueios explícitos no método de acesso. A instância única é criada dentro de uma classe estática interna, que só é carregada na memória quando o método obterGerenciador é invocado pela primeira vez, garantindo a enicialização preguiçosa e a segurança de threads devido à forma como os class loaders do Java funcionam.
package com.example.designpatterns;
public class GerenciadorLog {
// Construtor privado.
private GerenciadorLog() {
System.out.println("GerenciadorLog instanciado.");
}
// Classe interna estática que detém a instância única.
private static class DetentorInstancia {
private static final GerenciadorLog INSTANCIA = new GerenciadorLog();
}
// O método de acesso é chamado para inicializar DetentorInstancia.
public static GerenciadorLog obterGerenciador() {
return DetentorInstancia.INSTANCIA;
}
public void escreverLog(String entrada) {
System.out.println("Escrevendo no log: " + entrada);
}
}
- Singleton com Enum
A partir do Java 5, a implementação de Singleton usando um enum se tornou a maneira mais simples e robusta de garantir o padrão. Essa abordagem é intrinsecamente thread-safe, resistente à serialização/desserialização e imune a ataques de reflexão que podem violar o padrão Singleton em outras implementações.
package com.example.designpatterns;
public enum ConfiguracaoAplicacao {
// A única instância do enum.
INSTANCIA;
private String tema = "default";
private ConfiguracaoAplicacao() {
System.out.println("ConfiguracaoAplicacao enum instanciado.");
}
public void definirTema(String novoTema) {
this.tema = novoTema;
System.out.println("Tema definido para: " + this.tema);
}
public String obterTema() {
return this.tema;
}
}