É comum sabermos que o Spring funciona com todos os seus componentes criados como singleton, mas como é que isso funciona com sua aplicação trabalhando com múltiplas requisições usando a mesma instância de bean singleton? A ideia desta breve explicação é jogar luz sobre como as coisas funcionam de fato por baixo dos panos.
Primeiro, vamos matar uma ideia errada que circula por aí: "Beans singleton significam single-threaded". Isso não é correto.
Um bean singleton significa apenas uma instância dentro do contexto do Spring. Mas essa instância pode ser usada por centenas de threads simultaneamente sem problema algum. Vamos entender o porquê disso.
A Mágica em 3 Camadas
Para realmente compreender isso, precisamos pensar além do nível da aplicação. Vamos descer um pouco:
1. Nível Aplicação (Spring + Java)
No seu servidor rodando Spring, em configuração padrão acontece isso:
- Spring usa Tomcat com um pool de threads (padrão: até 200 threads)
- Cada requisição HTTP chega em uma thread diferente
- Todos os seus beans são singleton (uma única instância cada)
- Os beans não carregam estado mutável (são stateless)
Simples assim.
2. Nível JVM
Aqui é onde fica interessante. A JVM divide a memória em algumas regiões, mas vamos abordar as duas regiões principais:
┌─────────────────────────────────────┐
│ MEMÓRIA NA JVM │
├─────────────────────────────────────┤
│ HEAP (compartilhada) │
│ • Objetos singleton │
│ • Código dos métodos │
│ • Campos de instância │
│ │
│ STACK (isolada por thread) │
│ • Thread 1: variáveis locais │
│ • Thread 2: variáveis locais │
│ • Thread 3: variáveis locais │
└─────────────────────────────────────┘
A fórmula mágica está aqui: CÓDIGO (heap) + DADOS LOCAIS (stack) = Thread-Safe
O código fica no heap, compartilhado entre todas as threads. Mas os dados locais (variáveis de método) ficam na stack de cada thread isoladamente. Logo, não há conflito.
3. Nível Hardware
No processador, funciona mais ou menos assim:
- Seu objeto singleton vive em um endereço fixo na RAM (algo como
0x7f8a2c00) - Todas as threads leem desse endereço simultaneamente
- Cada núcleo de CPU mantém uma cópia em cache
- Leitura é naturalmente thread-safe, escrita precisa sincronização
Por Que Funciona
Vamos pegar um exemplo real:
@Service
public class CustomerService {
@Autowired
private CustomerRepository repository; // ← Fica na HEAP, só leitura ✅
public CustomerResponse evaluate(String customerName, String locationId) {
Customer customer = repository.findByName(customerName); // ← Variável local, STACK ✅
boolean result = checkConditions(customer, locationId); // ← Variável local, STACK ✅
return new CustomerResponse(result); // ← Novo objeto, STACK ✅
}
}
Veja só o que tá acontecendo:
-
repository(injetada): Está na heap, todas as threads leem ela. Mas como ninguém tá modificando, fica tranquilo. -
customer: É uma variável local, fica na stack de cada thread isoladamente. -
result: Mesma coisa, stack isolada. - Retorno: Um novo objeto criado para essa requisição específica.
Zero race conditions. Totalmente isolado.
Visualizando Tudo Junto
Imagina que 3 requisições chegam ao mesmo tempo no seu servidor:
T1: Request 1 → evaluationService.evaluate("feat-x", "client-1")
T2: Request 2 → evaluationService.evaluate("feat-y", "client-2")
T3: Request 3 → evaluationService.evaluate("feat-x", "client-3")
┌──────────────────────────────┐
│ HEAP │
│ EvaluationService @ 0x7f... │ ← 1 instância
│ └─ repository │
└──────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ STACK T1 │ │ STACK T2 │ │ STACK T3 │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ toggle ref │ │ toggle ref │ │ toggle ref │
│ result │ │ result │ │ result │
│ clientId │ │ clientId │ │ clientId │
└──────────────┘ └──────────────┘ └──────────────┘
Cada uma isolada
Cada thread tem seu próprio espaço na stack. Não tem colisão.
O Lado Perigoso
Mas claro que tem um "mas". Olha isso:
@Service
public class CounterService {
private int counter = 0; // ❌ CAMPO MUTÁVEL COMPARTILHADO!
public void increment() {
counter++; // ❌ RACE CONDITION!
}
}
Aqui sim teremos problema. Por quê? Porque counter está na heap, compartilhada entre as threads. Se duas threads tentarem incrementar ao mesmo tempo, uma pode sobrescrever o valor da outra.
Solução: Usar synchronized, AtomicInteger, ou simplesmente não manter estado na classe. Mantenha a classe sempre stateless.
Timeline de Execução Completa
Para deixar cristalino, vamos rastrear uma requisição do começo ao fim:
T0: Requisição chega via HTTP
T1: Tomcat pega uma thread do pool → Thread-123
T2: DispatcherServlet roteia para controller
T3: Controller injeta dependências (todas singleton, heap)
→ Todas as threads acessam o MESMO endereço de memória
T4: Entra no método da service
→ Cria variáveis locais (STACK isolada da T-123)
T5: Faz chamada ao banco
→ Mesma thread, stack isolada
T6: Processa resultado
→ Completamente local
T7: Retorna resposta
→ Nova instância de resposta criada (heap, mas única para essa req)
T8: Response é serializada e enviada
→ Thread-123 volta pro pool, pronta para próxima requisição
Repare: Mesmo que outras threads estejam processando outras requisições ao mesmo tempo, cada uma tem seu próprio contexto de execução isolado. Sem conflito.
A Regra de Ouro
E se tu fosse me resumir tudo em uma frase, seria:
"Mantenha seus beans STATELESS e o Spring cuida do resto automaticamente."
Sem campos mutáveis compartilhados, não tem sincronização necessária. Simples demais para ser verdade, mas é.
Comparação Rápida
Só para consolidar tudo:
| Aspecto | Singleton Bean | Comportamento |
|---|---|---|
| Instâncias | 1 objeto | Compartilhado entre threads |
| Threads simultâneas | 200+ | Executam em paralelo tranquilamente |
| Campos de instância | Heap (compartilhados) | ⚠️ Perigoso se mutáveis |
| Variáveis locais | Stack (isoladas) | ✅ Thread-safe automático |
| Métodos | Heap (código compartilhado) | ✅ Seguro, só lê |
| Objetos retornados | Heap (sempre novos por req) | ✅ Isolado por requisição |
Resumindo Tudo
A arquitetura do Spring Boot é linda assim:
- Singleton beans economizam memória
- Design stateless garante segurança
- Pool de threads permite processamento paralelo
- Isolamento de stack protege variáveis locais
Top comments (0)