O que é Singleton (e o que ele realmente faz)
Singleton é um padrão cujo objetivo é garantir que exista uma única instância de uma classe em todo o processo e oferecer um ponto de acesso global a ela. Em Java, isso normalmente aparece como um método getInstance() que retorna sempre o mesmo objeto.
O ponto crítico: o Singleton não é apenas “uma instância única”. Ele também cria um acoplamento global (qualquer parte do código pode acessar), o que afeta testabilidade, previsibilidade e evolução do design.
Quando Singleton pode ser aceitável
1) Recurso realmente único no processo
Alguns recursos são naturalmente únicos por processo e não fazem sentido em múltiplas instâncias, por exemplo:
- Um gerenciador de acesso a um dispositivo/porta (quando o próprio recurso é exclusivo).
- Um registrador de métricas central (desde que seja bem encapsulado e thread-safe).
2) Configuração imutável e estável
Se a aplicação carrega uma configuração imutável (por exemplo, a partir de variáveis de ambiente) e ela é usada em vários pontos, um Singleton pode ser aceitável, desde que:
- O objeto seja imutável (ou efetivamente imutável).
- O ciclo de vida seja claro (carrega uma vez, não muda).
- O acesso global não vire “atalho” para evitar passar dependências.
Quando Singleton tende a ser prejudicial
1) Estado global mutável
Se o Singleton guarda estado que muda (cache mutável, flags, usuário atual, “modo debug”, contadores sem sincronização), você introduz:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
- Dependências ocultas (qualquer classe pode alterar o estado).
- Comportamento difícil de prever (ordem de execução importa).
- Risco de condições de corrida em concorrência.
2) Testes difíceis e frágeis
Singleton frequentemente dificulta testes porque:
- Você não consegue trocar facilmente a implementação por um stub/fake.
- O estado pode “vazar” entre testes (um teste influencia o outro).
- É comum precisar de hacks (reset de singleton, reflection) para isolar cenários.
3) Ciclo de vida e recursos (leaks)
Se o Singleton abre recursos (threads, conexões, file handles) e não tem um ciclo de vida explícito, ele pode:
- Manter recursos abertos até o fim do processo.
- Impedir descarte/recarga controlada (por exemplo, em testes ou em ferramentas).
Implementações seguras de Singleton em Java
Opção A: Inicialização estática (eager initialization)
Simples e thread-safe por definição do carregamento de classes em Java. Bom quando o custo de criar a instância é baixo e você tem certeza de que ela será usada.
public final class AppConfigSingleton { private static final AppConfigSingleton INSTANCE = new AppConfigSingleton(); private final String apiBaseUrl; private AppConfigSingleton() { this.apiBaseUrl = System.getenv().getOrDefault("API_BASE_URL", "http://localhost"); } public static AppConfigSingleton getInstance() { return INSTANCE; } public String apiBaseUrl() { return apiBaseUrl; }}Passo a passo do que torna isso seguro:
private static finalcria a instância uma vez.- Construtor
privateimpedenewexterno. - A JVM garante inicialização segura e visibilidade entre threads ao carregar a classe.
Opção B: Holder idiom (lazy e thread-safe)
Cria a instância apenas quando for realmente usada, mantendo thread-safety sem sincronização explícita.
public final class MetricsRegistry { private MetricsRegistry() {} private static class Holder { private static final MetricsRegistry INSTANCE = new MetricsRegistry(); } public static MetricsRegistry getInstance() { return Holder.INSTANCE; } // Exemplo de API (evite estado mutável sem sincronização) public void record(String name, long value) { // implementar com estruturas thread-safe, se necessário }}Passo a passo:
- A classe interna
Holdersó é carregada quandogetInstance()é chamado. - O carregamento de classe é thread-safe, então a instância é criada uma única vez.
Opção C: Enum Singleton (recomendado quando apropriado)
Em Java, enum é uma forma robusta de Singleton: simplifica serialização e evita problemas comuns de reflection/duplicação.
public enum ClockSingleton { INSTANCE; public long nowMillis() { return System.currentTimeMillis(); }}Quando usar enum:
- Quando a instância é realmente única e você não precisa herdar de outra classe.
- Quando quer simplicidade e robustez contra armadilhas de serialização.
Quando evitar enum:
- Quando você precisa controlar ciclo de vida (inicializar/encerrar recursos explicitamente).
- Quando quer substituir facilmente em testes (ainda dá, mas tende a ser mais rígido).
Concorrência: o que pode dar errado (e como evitar)
1) Singleton thread-safe não significa “estado interno thread-safe”
Mesmo que a instância seja criada de forma segura, o que ela faz por dentro pode não ser. Exemplo de problema: contador mutável sem sincronização.
public final class BadCounterSingleton { private static final BadCounterSingleton INSTANCE = new BadCounterSingleton(); private int count = 0; private BadCounterSingleton() {} public static BadCounterSingleton getInstance() { return INSTANCE; } public void inc() { count++; // condição de corrida } public int get() { return count; }}Se for inevitável manter estado mutável compartilhado, use estruturas apropriadas (por exemplo, AtomicInteger, locks, coleções concorrentes) e defina claramente as garantias de thread-safety.
2) Lazy com double-checked locking (use com cuidado)
É possível implementar lazy com volatile, mas é mais fácil errar e geralmente é menos legível do que o Holder idiom.
public final class LazySingleton { private static volatile LazySingleton instance; private LazySingleton() {} public static LazySingleton getInstance() { LazySingleton local = instance; if (local == null) { synchronized (LazySingleton.class) { local = instance; if (local == null) { local = new LazySingleton(); instance = local; } } } return local; }}Prefira Holder ou enum quando possível.
Ciclo de vida: inicialização, descarte e recursos
Singleton costuma “viver para sempre” no processo. Isso é aceitável para objetos leves e imutáveis, mas perigoso para objetos que:
- Abrem conexões (banco, sockets).
- Iniciam threads/schedulers.
- Precisam ser reinicializados (troca de ambiente, testes, recarga).
Se o objeto precisa de start/stop, normalmente um Singleton não é a melhor escolha. Um ciclo de vida explícito (criar, usar, fechar) tende a ser mais saudável.
Alternativas melhores em muitos casos
1) Injeção explícita de dependências (sem framework)
Em vez de chamar Singleton.getInstance() dentro das classes, passe a dependência por construtor. Isso torna a dependência visível e substituível em testes.
Exemplo: serviço que precisa de relógio
public interface Clock { long nowMillis();}public final class SystemClock implements Clock { @Override public long nowMillis() { return System.currentTimeMillis(); }}public final class BillingService { private final Clock clock; public BillingService(Clock clock) { this.clock = clock; } public boolean isLate(long dueMillis) { return clock.nowMillis() > dueMillis; }}Uso na aplicação:
Clock clock = new SystemClock();BillingService service = new BillingService(clock);Uso em teste (fake):
final class FakeClock implements Clock { private final long fixed; FakeClock(long fixed) { this.fixed = fixed; } public long nowMillis() { return fixed; }}BillingService service = new BillingService(new FakeClock(1_700_000_000_000L));2) Objeto imutável compartilhado (sem “global” escondido)
Se a motivação do Singleton é “não quero criar várias vezes”, muitas vezes basta criar uma instância imutável e compartilhar por composição, mantendo o acesso explícito.
public record AppConfig(String apiBaseUrl, int timeoutMillis) {}public final class Application { private final AppConfig config; public Application(AppConfig config) { this.config = config; } // repassa config para quem precisa, explicitamente}3) Factory/Provider explícito
Quando você quer controlar criação e ciclo de vida, um Provider explícito pode ser melhor do que um Singleton global. Você pode ter um “composition root” que monta tudo.
public final class Services { private final Clock clock; private final BillingService billingService; public Services() { this.clock = new SystemClock(); this.billingService = new BillingService(clock); } public BillingService billingService() { return billingService; }}Isso mantém “uma instância por aplicação” sem transformar em estado global acessível de qualquer lugar.
Exercício: comparar abordagens e avaliar testabilidade
Contexto
Você precisa implementar um componente que gera tokens com validade baseada no tempo atual. Compare duas abordagens: (A) usando Singleton para o relógio; (B) usando injeção explícita.
Parte 1 — Implementação com Singleton (para análise crítica)
Implemente:
ClockSingleton(pode ser enum) comnowMillis().TokenServiceque chamaClockSingleton.INSTANCE.nowMillis()para calcular expiração.
public enum ClockSingleton { INSTANCE; public long nowMillis() { return System.currentTimeMillis(); }}public final class TokenService { public String issueToken(long ttlMillis) { long exp = ClockSingleton.INSTANCE.nowMillis() + ttlMillis; return "exp=" + exp; }}Tarefas:
- Escreva um teste que valide que o token expira corretamente. Observe a dificuldade de controlar o tempo.
- Liste pelo menos 3 problemas potenciais dessa abordagem (ex.: dependência oculta, testes flakey, acoplamento global).
Parte 2 — Implementação com injeção explícita (recomendado)
Refatore para:
- Interface
Clock. SystemClockeFakeClock.TokenServicerecebendoClockno construtor.
public interface Clock { long nowMillis();}public final class SystemClock implements Clock { public long nowMillis() { return System.currentTimeMillis(); }}public final class TokenService { private final Clock clock; public TokenService(Clock clock) { this.clock = clock; } public String issueToken(long ttlMillis) { long exp = clock.nowMillis() + ttlMillis; return "exp=" + exp; }}Tarefas:
- Crie
FakeClockcom tempo fixo e escreva um teste determinístico paraissueToken. - Compare a legibilidade e a facilidade de manutenção.
Parte 3 — Checklist de avaliação
| Critério | Singleton | Injeção explícita |
|---|---|---|
| Dependência visível no construtor | Não | Sim |
| Facilidade de substituir em testes | Baixa | Alta |
| Risco de estado global acidental | Maior | Menor |
| Controle de ciclo de vida | Limitado | Explícito |
| Concorrência (estado interno) | Frequentemente negligenciada | Mais fácil de isolar |