7 Implementações e Práticas do Padrão Singleton em Java

Padrão Singleton: Garante que apenas uma única instância de uma classe exista em todo o processo.

  1. 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.

  1. 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.

  1. 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!

  1. 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.

  1. 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:

  1. Alocação de espaço. Espaço alocado no heap.
  2. Execução do construtor. Chamada do construtor privado SingletonVolatileTest.
  3. 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".

  1. 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.

  1. 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".

  1. 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!

Tags: java Padrão Singleton Concorrência Otimização de Código Design de Software

Publicado em 6-26 20:48