DEV Community

Marcos Faneli
Marcos Faneli

Posted on

Spring e Multithreading: Um pouco sobre como Beans Singleton lidam com múltiplas requisições

É 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       │
└─────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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 ✅
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Singleton beans economizam memória
  2. Design stateless garante segurança
  3. Pool de threads permite processamento paralelo
  4. Isolamento de stack protege variáveis locais

Top comments (0)