← Todos os padrões

Singleton

Creational · ver código-fonte no GitHub

Leia em: English · Português · Español

Category: Creational

O problema

Alguns recursos genuinamente precisam de exatamente uma instância compartilhada por processo: um registro de configuração, um pool de conexões, uma tabela de limites regulatórios. Se cada chamador constrói sua própria cópia, ou se desperdiça o custo de construí-la repetidamente ou — pior — partes diferentes do sistema acabam enxergando cópias diferentes, possivelmente desatualizadas, do que deveria ser uma única fonte de verdade. Acertar essa garantia de "uma instância" sob acesso concorrente é mais difícil do que parece: uma checagem ingênua if (instance == null) instance = new Thing() tem uma condição de corrida em que duas threads podem passar pela checagem de nulo antes que qualquer uma tenha atribuído o campo.

A solução

Esconder o construtor, expor um único ponto de acesso, e tornar esse ponto de acesso seguro sob primeiro uso concorrente.

classDiagram
    class LazyThreadSafeSingleton {
        -static volatile instance
        -LazyThreadSafeSingleton()
        +static getInstance() LazyThreadSafeSingleton
        +getSetting(key) String
    }
    class Caller
    Caller --> LazyThreadSafeSingleton : getInstance()

Exemplo clássico

classic/LazyThreadSafeSingleton é o singleton de double-checked locking clássico dos livros: um campo volatile, uma checagem de nulo fora do lock (caminho rápido depois de inicializado), e uma segunda checagem de nulo dentro de um bloco synchronized (de modo que só a primeira thread que passa de fato constrói a instância). O campo precisa ser volatile — sem isso, uma thread poderia observar uma referência não nula a um objeto cujo construtor ainda não terminou de escrever seus campos, porque a JVM tem permissão de reordenar a escrita em instance antes das escritas que acontecem dentro do construtor.

LazyThreadSafeSingletonTest dispara 50 threads em getInstance() simultaneamente (sincronizadas com um CountDownLatch pra que de fato disputem na primeira chamada) e verifica que toda thread observou exatamente a mesma instância.

Exemplo aplicado: registro de limites regulatórios do PIX

applied/HandRolledLimitRegistry modela uma tabela central de limites do PIX definidos pelo BACEN (teto diário, teto noturno reduzido) que todo validador de transação concorrente lê. Recarregar esses limites a cada chamada de validação seria um desperdício, e validadores rodando concorrentemente precisam todos enxergar os mesmos valores — exatamente o cenário pro qual o padrão existe, aplicado com a mesma mecânica de double-checked locking do exemplo clássico.

applied/SpringManagedLimitRegistry implementa o mesmo contrato LimitRegistry sem nenhuma maquinaria de singleton — é uma classe simples. A garantia de instância única vem inteiramente do escopo de bean padrão do Spring (singleton), conectado em SingletonRegistryConfig. SpringManagedLimitRegistryTest prova isso: duas chamadas a context.getBean(...) retornam a mesma referência, com zero código de locking escrito à mão. Mesma garantia, duas formas de obtê-la — uma você constrói você mesmo, a outra um container te dá de graça uma vez que você aceita a dependência.

Quando não usar

Cobertura de testes

97% de cobertura de instrução, 87% de cobertura de branch (JaCoCo). Reproduza você mesmo:

./gradlew :creational:singleton:jacocoTestReport

Relatório em creational/singleton/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

src/test/java/com/designpatterns/creational/singleton/classic/LazyThreadSafeSingletonTest.java
package com.designpatterns.creational.singleton.classic;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Ordered on purpose: the concurrency test below must be the one that constructs the
 * singleton (all 50 threads racing through the synchronized block), not a coincidence of
 * JUnit's default method order. If {@code exposesTheSettingsLoadedAtConstruction} ran first,
 * it would construct the instance single-threaded, and the "concurrent" test would never
 * actually exercise contention on the lock - it would just find the instance already there.
 */
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class LazyThreadSafeSingletonTest {

    @Test
    @Order(1)
    void returnsTheSameInstanceUnderConcurrentFirstAccess() throws InterruptedException {
        int threadCount = 50;
        ExecutorService pool = Executors.newFixedThreadPool(threadCount);
        CountDownLatch ready = new CountDownLatch(threadCount);
        CountDownLatch start = new CountDownLatch(1);
        Set<LazyThreadSafeSingleton> seenInstances = Collections.newSetFromMap(new ConcurrentHashMap<>());

        try {
            for (int i = 0; i < threadCount; i++) {
                pool.submit(() -> {
                    ready.countDown();
                    awaitUninterruptibly(start);
                    seenInstances.add(LazyThreadSafeSingleton.getInstance());
                });
            }

            assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue();
            start.countDown();
            pool.shutdown();
            assertThat(pool.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
        } finally {
            pool.shutdownNow();
        }

        assertThat(seenInstances).hasSize(1);
    }

    @Test
    @Order(2)
    void exposesTheSettingsLoadedAtConstruction() {
        assertThat(LazyThreadSafeSingleton.getInstance().getSetting("environment")).isEqualTo("production");
    }

    private static void awaitUninterruptibly(CountDownLatch latch) {
        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
src/test/java/com/designpatterns/creational/singleton/applied/HandRolledLimitRegistryTest.java
package com.designpatterns.creational.singleton.applied;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Ordered on purpose - see {@code LazyThreadSafeSingletonTest} for why: the concurrency test
 * must be the one that actually constructs the singleton under contention, not whichever test
 * happens to call getInstance() first.
 */
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class HandRolledLimitRegistryTest {

    @Test
    @Order(1)
    void everyConcurrentValidatorSeesTheSameRegistryInstance() throws InterruptedException {
        int threadCount = 50;
        ExecutorService pool = Executors.newFixedThreadPool(threadCount);
        CountDownLatch ready = new CountDownLatch(threadCount);
        CountDownLatch start = new CountDownLatch(1);
        Set<HandRolledLimitRegistry> seenInstances = Collections.newSetFromMap(new ConcurrentHashMap<>());

        try {
            for (int i = 0; i < threadCount; i++) {
                pool.submit(() -> {
                    ready.countDown();
                    awaitUninterruptibly(start);
                    seenInstances.add(HandRolledLimitRegistry.getInstance());
                });
            }

            assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue();
            start.countDown();
            pool.shutdown();
            assertThat(pool.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
        } finally {
            pool.shutdownNow();
        }

        assertThat(seenInstances).hasSize(1);
    }

    @Test
    @Order(2)
    void exposesTheRegulatoryLimits() {
        LimitRegistry registry = HandRolledLimitRegistry.getInstance();

        assertThat(registry.dailyLimitCents()).isEqualTo(100_000_00L);
        assertThat(registry.nightlyLimitCents()).isEqualTo(1_000_00L);
    }

    private static void awaitUninterruptibly(CountDownLatch latch) {
        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
src/test/java/com/designpatterns/creational/singleton/applied/SpringManagedLimitRegistryTest.java
package com.designpatterns.creational.singleton.applied;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

class SpringManagedLimitRegistryTest {

    private AnnotationConfigApplicationContext context;

    @AfterEach
    void closeContext() {
        if (context != null) {
            context.close();
        }
    }

    @Test
    void springReturnsTheSameBeanInstanceOnEveryLookup() {
        context = new AnnotationConfigApplicationContext(SingletonRegistryConfig.class);

        SpringManagedLimitRegistry first = context.getBean(SpringManagedLimitRegistry.class);
        SpringManagedLimitRegistry second = context.getBean(SpringManagedLimitRegistry.class);

        assertThat(first).isSameAs(second);
        assertThat(first.dailyLimitCents()).isEqualTo(100_000_00L);
    }
}

Ver relatório completo de cobertura JaCoCo →