Em sistemas concorrentes, múltiplas threads ou processos frequentemente precisam acessar os mesmos recursos. Quando isso acontece de forma descontrolada, surge a contenção — uma disputa que pode devastar a performance do sistema.
Este artigo explora o que é contenção, como ela se manifesta, e estratégias para minimizá-la.
Contenção é o preço da concorrência mal gerenciada.
O que é Contenção
Contenção ocorre quando múltiplos processos ou threads competem por um recurso limitado que só pode ser usado por um de cada vez.
Exemplos de recursos disputados
- Locks/mutexes: apenas uma thread pode segurar por vez
- Conexões de banco: pool limitado
- CPU cores: mais threads que cores
- I/O de disco: uma operação por vez no mesmo arquivo
- Rede: bandwidth limitado
O custo da contenção
Quando há contenção:
- Threads ficam esperando em vez de trabalhando
- CPU gasta tempo em context switching
- Throughput cai mesmo com recursos disponíveis
- Latência aumenta de forma imprevisível
Tipos de Contenção
Contenção de Lock
A mais comum em código.
synchronized (this) {
// Apenas uma thread por vez
processRequest();
}
Se processRequest() demora 10ms e chegam 100 requisições/segundo:
- 100 × 10ms = 1000ms de trabalho por segundo
- Apenas 1 thread pode executar = gargalo severo
Contenção de CPU
Mais threads ativas que cores disponíveis.
8 cores, 100 threads ativas
= Muito context switching
= Overhead significativo
Contenção de I/O
Múltiplos processos tentando ler/escrever simultaneamente.
Thread 1: write(file, data1)
Thread 2: write(file, data2) // Espera thread 1
Thread 3: write(file, data3) // Espera thread 2
Contenção de conexão
Pool de conexões esgotado.
Pool: 10 conexões
Requisições simultâneas: 50
= 40 requisições esperando
Identificando Contenção
Sintomas
- CPU baixa, latência alta: threads esperando, não trabalhando
- Throughput não escala: mais threads não significa mais trabalho
- Latência errática: depende de quem chegou primeiro
- Lock contention metrics: ferramentas de profiling mostram espera
Ferramentas de diagnóstico
Java:
jstackpara ver threads em BLOCKED- JFR (Java Flight Recorder) para lock contention
- VisualVM
Linux:
perfpara CPU profilingstracepara I/O/proc/[pid]/statuspara context switches
Métricas a monitorar:
- Threads em estado BLOCKED/WAITING
- Context switches por segundo
- Tempo médio de espera por lock
Estratégias de Mitigação
1. Reduza o escopo do lock
Antes:
synchronized (this) {
validateInput(data);
processData(data);
saveToDatabase(data);
sendNotification(data);
}
Depois:
validateInput(data); // Sem lock
Data processed = processData(data); // Sem lock
synchronized (this) {
saveToDatabase(processed); // Apenas o necessário
}
sendNotification(data); // Sem lock
2. Use estruturas lock-free
// Em vez de
synchronized (counter) {
counter++;
}
// Use
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
3. Particione recursos
Em vez de um lock global, use locks por partição.
// Em vez de
synchronized (cache) {
cache.put(key, value);
}
// Use locks por segmento
int segment = key.hashCode() % NUM_SEGMENTS;
synchronized (locks[segment]) {
segments[segment].put(key, value);
}
ConcurrentHashMap faz exatamente isso.
4. Aumente pools
Se conexões são o gargalo, aumente o pool — mas com cuidado:
- Cada conexão consome memória
- Backend precisa suportar mais conexões
5. Use operações assíncronas
// Em vez de esperar
CompletableFuture<Result> future = processAsync(data);
// Continue fazendo outras coisas
future.thenAccept(result -> handleResult(result));
6. Batch operations
// Em vez de
for (Item item : items) {
synchronized (lock) {
save(item);
}
}
// Faça batch
synchronized (lock) {
saveAll(items);
}
Contenção em Banco de Dados
Row-level locking
-- Transação 1
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Transação 2 (espera transação 1)
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
Mitigações:
- Transações curtas
- Ordenar acessos consistentemente
- Usar níveis de isolamento adequados
Table-level locking
Algumas operações bloqueiam tabelas inteiras:
ALTER TABLELOCK TABLE- Índices sendo criados
Deadlocks
Quando duas transações esperam uma pela outra:
T1: lock(A), espera lock(B)
T2: lock(B), espera lock(A)
= Deadlock
Prevenção:
- Sempre adquirir locks na mesma ordem
- Timeout em transações
- Detectar e resolver automaticamente
Lei de Amdahl e Contenção
A Lei de Amdahl mostra que a parte sequencial limita o ganho de paralelização:
Speedup máximo = 1 / (S + (1-S)/N)
S = fração sequencial (contenção)
N = número de processadores
Se 10% do código é sequencial (contenção):
- Com 10 cores: speedup máximo = 5.3x
- Com 100 cores: speedup máximo = 9.2x
- Com 1000 cores: speedup máximo = 9.9x
Conclusão: reduzir contenção tem mais impacto que adicionar recursos.
Boas Práticas
- Meça antes de otimizar: nem toda contenção é problema
- Minimize seções críticas: faça o mínimo dentro de locks
- Prefira estruturas concorrentes: ConcurrentHashMap, AtomicInteger
- Evite locks aninhados: fonte de deadlocks
- Use timeouts: não espere infinitamente
- Monitore continuamente: contenção pode surgir com escala
Conclusão
Contenção é um dos gargalos mais sutis e impactantes em sistemas concorrentes. Ela:
- Limita escalabilidade
- Aumenta latência
- Desperdiça recursos
Para combatê-la:
- Identifique onde ocorre (profiling)
- Reduza seções críticas
- Particione recursos quando possível
- Use estruturas adequadas para concorrência
Paralelismo sem contenção é velocidade. Paralelismo com contenção é desperdício.