Padrão Singleton: Garante que apenas uma única instância de uma classe exista em todo o processo.
- Modagem Eager (Início Ansioso)
/**
* Implementação do padrão Singleton Eager (Eager Initialization)
*/
public class SingletonEager {
// Instância é criada durante a inicialização da classe
private static final SingletonEager instancia = new SingletonEager();
private SingletonEager() {}
public static SingletonEager getInstancia() {
return instancia;
}
public static void main(String[] args) {
// Teste com três threads para verificar se o hashcode é o mesmo
new Thread(() -> {System.out.println(SingletonEager.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonEager.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonEager.getInstancia().hashCode());}).start();
}
}
A modagem Eager cria o objeto proativamente, como mostrado no código acima. No ambiente JDK1.8, quando a thread principal inicia três threads para obter a instância de SingletonEager, todos os hashcodes são idênticos, provando que existe apenas uma instância no programa. A implementação de Singleton Eager cria o objeto antecipadamente durante a inicialização da classe. Desvantagem: A criação prematura do objeto consome recursos de memória desnecessários, pois precisamos criar o objeto singleton apenas quando ele for necessário. Vamos ver a implementação da modagem Lazy a seguir.
- Modagem Lazy (Início Preguiçoso) - Não Thread-Safe
/**
* Implementação do padrão Singleton Lazy (Lazy Initialization) - Não thread-safe
*/
public class SingletonLazy {
private static SingletonLazy instancia = null;
private SingletonLazy() {}
public static SingletonLazy getInstancia() {
if (instancia == null) {
/*try {
// Pausa de 2ms durante a criação do objeto
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
instancia = new SingletonLazy();
}
return instancia;
}
public static void main(String[] args) {
// Teste com três threads
new Thread(() -> {System.out.println(SingletonLazy.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonLazy.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonLazy.getInstancia().hashCode());}).start();
}
}
A modagem Lazy cria o objeto apenas quando o método getInstance() é chamado. Parece não ter problemas, mas se liberarmos a instrução de pausa no código acima, com uma pausa de 2ms durante a criação do objeto, os resultados podem mostrar hashcodes diferentes para as três threads, indicando que o objeto não é singleton. Em situações de atraso, todas as threads entram na instrução if, resultando na criação de múltiplas instâncias. Desvantagem: Não é thread-safe, precisamos adicionar um bloqueio. Vamos modificar o método getInstance() com a palavra-chave synchronized:
public static synchronized SingletonLazy getInstancia() {
if (instancia == null) {
instancia = new SingletonLazy();
}
return instancia;
}
Com o método sincronizado, as três threads imprimirão hashcodes consistentes. Teste concluído com sucesso! Ainda não terminou? Observe que synchronized modifica o método inteiro, o que tem um grande impacto de bloqueio. Precisamos bloquear apenas durante a criação do objeto. Para programadores que buscam otimização de código, precisamos "aperfeiçoar" até o último detalhe. Vamos ver a implementação com synchronized em um bloco de código.
- Modagem Lazy com Bloqueio - Ainda Não Thread-Safe
/**
* Implementação do padrão Singleton Lazy com bloqueio - Não thread-safe
*/
public class SingletonLazyLock {
private static SingletonLazyLock instancia = null;
private SingletonLazyLock() {}
public static SingletonLazyLock getInstancia() {
if (instancia == null) {
/*try {
// Pausa de 2 segundos durante a criação do objeto
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
synchronized (SingletonLazyLock.class) {
instancia = new SingletonLazyLock();
}
}
return instancia;
}
public static void main(String[] args) {
// Teste com três threads
new Thread(() -> {System.out.println(SingletonLazyLock.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonLazyLock.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonLazyLock.getInstancia().hashCode());}).start();
}
}
O padrão de verificação única com bloqueio parece não ter problemas, mas se liberarmos o código de pausa acima, com uma pausa de 2 segundos durante a criação do objeto, os resultados podem mostrar hashcodes diferentes para as três threads, indicando que o objeto não é singleton. Por que isso acontece? Porque as três threads são bloqueadas antes da criação do objeto, pois todas já verificaram que a instância é nula, então cada uma criará uma nova instância! Ok, se esse é o caso, podemos adicionar outra verificação dentro do bloco de código sincronizado para garantir total segurança! Este é o mecanismo de verificação dupla com bloqueio, um problema clássico em entrevistas! Vamos modificar o código para implementar o mecanismo de verificação dupla com bloqueio. Isso garante total segurança? Vejamos o código! A prática prova tudo!
- Verificação Dupla com Bloqueio - Reordenação de Instruções
/**
* Implementação do padrão Singleton com verificação dupla de bloqueio
*/
public class SingletonDoubleCheck {
private static SingletonDoubleCheck instancia = null;
private SingletonDoubleCheck() {}
public static SingletonDoubleCheck getInstancia() {
if (instancia == null) {
synchronized (SingletonDoubleCheck.class) {
if (instancia == null) {
instancia = new SingletonDoubleCheck();
}
}
}
return instancia;
}
public static void main(String[] args) {
// Teste com três threads
new Thread(() -> {System.out.println(SingletonDoubleCheck.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonDoubleCheck.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonDoubleCheck.getInstancia().hashCode());}).start();
}
}
Com a verificação dupla, o primeiro if verifica se a instância é nula antes de bloquear. Após múltiplos testes, os hashcodes consistentes provam que apenas um objeto existe no processo. Parece sem problemas! Realmente é assim? Vamos fazer um teste aprofundado no código acima.
- Necessidade do volatile na Verificação Dupla com Bloqueio
import java.util.concurrent.CountDownLatch;
/**
* Teste de Singleton sem volatile para verificar reordenação de instruções
*/
public class SingletonVolatileTest {
private static SingletonVolatileTest instancia = null;
public int valor;
private SingletonVolatileTest(){
valor = 5;
};
public static SingletonVolatileTest getInstancia() {
if (instancia == null) {
synchronized (SingletonVolatileTest.class) {
if (instancia == null) {
instancia = new SingletonVolatileTest();
}
}
}
return instancia;
}
public static void resetar() {
instancia = null;
}
public static void main(String[] args) throws InterruptedException {
// Loop infinito criando múltiplas threads para testar o singleton
while (true) {
CountDownLatch inicio = new CountDownLatch(1);
CountDownLatch fim = new CountDownLatch(100);
for (int i=0; i<100; i++) {
Thread thread = new Thread(() -> {
try {
// Todas as threads esperam simultaneamente
inicio.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Verifica se o valor do singleton foi alterado por reordenação
if (SingletonVolatileTest.getInstancia().valor != 5) {
System.out.println("Reordenação detectada! Thread encerrada.");
System.exit(0);
}
fim.countDown();
});
thread.start();
}
inicio.countDown();
fim.await();
resetar();
}
}
}
Como mostrado no código acima: no programa principal, um loop infinito cria múltiplas threads que geram instâncias de singleton. A variável "valor" é definida para testar se o objeto SingletonVolatileTest() sofreu reordenação. A instrução new geralmente pode ser dividida em 3 etapas no JVM:
- Alocação de espaço. Espaço alocado no heap.
- Execução do construtor. Chamada do construtor privado SingletonVolatileTest.
- Atribuição da referência. A referência 'instancia' aponta para o novo objeto.
Para otimização de desempenho, o JVM pode reordenar as etapas 2 e 3, executando na ordem 1->3->2. Em ambientes multithread, isso pode resultar no valor "valor" não sendo igual a 5, indicando que múltiplas instâncias podem existir no processo, violando o padrão singleton. A solução correta é修饰 a variável de instância com volatile, que proíbe a reordenação de instruções, garantindo que a instrução new seja executada na ordem 1->2->3. Esta é a necessidade de修饰 variáveis de instância com volatile. Para entender melhor as características do volatile, consulte o artigo "Compreendendo visibilidade, ordenação e atomicidade com volatile e synchronized".
- Classe Interna Estática - Criação Preguiçosa (Recomendado)
/**
* Implementação do padrão Singleton usando classe interna estática
*/
public class SingletonClasseInterna {
private SingletonClasseInterna(){}
public static SingletonClasseInterna getInstancia() {
return SingletonHolder.instancia;
}
private static class SingletonHolder {
private static final SingletonClasseInterna instancia = new SingletonClasseInterna();
}
public static void main(String[] args) {
// Teste com três threads
new Thread(() -> {System.out.println(SingletonClasseInterna.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonClasseInterna.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonClasseInterna.getInstancia().hashCode());}).start();
}
}
A classe interna estática cria o objeto apenas quando o método getInstance() é chamado. Após múltiplos testes, as três threads obtêm hashcodes idênticos, provando que o objeto é singleton. Esta implementação simples e segura é a nossa recomendação.
- Inicialização por Bloco Estático
/**
* Implementação do padrão Singleton usando bloco estático
*/
public class SingletonBlocoEstatico {
private static SingletonBlocoEstatico instancia;
// Bloco estático para inicialização do singleton
static {
instancia = new SingletonBlocoEstatico();
}
// Construtor privado
private SingletonBlocoEstatico(){}
// Método estático para obter a instância
public static SingletonBlocoEstatico getInstancia() {
return instancia;
}
public static void main(String[] args) {
// Teste com três threads
new Thread(() -> {System.out.println(SingletonBlocoEstatico.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonBlocoEstatico.getInstancia().hashCode());}).start();
new Thread(() -> {System.out.println(SingletonBlocoEstatico.getInstancia().hashCode());}).start();
}
}
Como mostrado no código acima, o bloco estático é chamado durante a inicialização da classe. Três threads obtêm o objeto singleton simultaneamente, e após testes repetidos, os hashcodes permanecem consistentes, provando que o bloco estático pode implementar o padrão singleton. Desvantagem: Criação proativa do objeto, semelhante à implementação "Eager".
- Enum
import java.sql.Connection;
/**
* Implementação do padrão Singleton usando enum
*/
enum FabricaBancoDados {
CONEXAO_FABRICA;
private Connection conexao;
private FabricaBancoDados(){
System.out.println("Inicializando conexão Connection");
// Inicialização da conexão omitida
}
public Connection getConexao() {
return conexao;
}
public static void main(String[] args) {
// Teste com três threads
new Thread(() -> {System.out.println(FabricaBancoDados.CONEXAO_FABRICA.getConexao());}).start();
new Thread(() -> {System.out.println(FabricaBancoDados.CONEXAO_FABRICA.getConexao());}).start();
new Thread(() -> {System.out.println(FabricaBancoDados.CONEXAO_FABRICA.getConexao());}).start();
}
}
Como mostrado no código acima, semelhante à implementação "Eager". A inicialização do enum carrega o construtor por padrão.
Resumo
Discutimos 7 implementações do padrão singleton. Os elementos essenciais são:
- Variável estática e privada
- Construtor privado
- Método estático público para obter a instância
Além disso, recomendamos a implementação usando classe interna estática por ser simples e eficiente. Também detalhamos a implementação com verificação dupla de bloqueio, explicando os problemas potenciais e suas causas. Se este artigo foi útil, por favor, curta e siga. Para questões ou dúvidas, deixe um comentário!
Problema residual: No teste da necessidade do volatile na verificação dupla, após múltiplos testes, não conseguimos reproduzir o caso onde o valor "valor" não é igual a 5. Experiências práticas são bem-vindas!