- No passado, o uso de CPU do meu sistema chegou a 3.200%, com todos os 32 núcleos totalmente ocupados
- Eu estava usando o runtime Java 17 e, ao verificar o tempo de CPU no dump de threads e ordenar por tempo de CPU, encontrei várias threads semelhantes
- Análise do código problemático
- Pelo stack trace, foi possível identificar a linha 29 da classe
BusinessLogic
- O código em questão percorria a lista
unrelatedObjects enquanto inseria o valor de relatedObject em treeMap
- Isso era um código ineficiente, pois não usava
unrelatedObject dentro do loop
Correção do código e testes
- O loop desnecessário foi removido e o código foi alterado para uma única linha:
treeMap.put(relatedObject.a(), relatedObject.b());
- Testes unitários foram executados antes e depois da alteração, mas não foi possível reproduzir o problema
- Mesmo quando
treeMap e unrelatedObjects tinham mais de 1.000.000 de itens cada, o problema não ocorria
Descoberta da causa do problema
treeMap estava sendo acessado simultaneamente por várias threads, sem sincronização
- O problema era causado por várias threads modificando o
TreeMap ao mesmo tempo
Reprodução do problema por meio de experimento
- Foi realizado um experimento em que várias threads atualizavam aleatoriamente um
TreeMap compartilhado
- Foi usado um bloco
try-catch para ignorar NullPointerException
- Como resultado do experimento, foi observado que o uso de CPU subia até 500%
Conclusão
- Modificações concorrentes em um
TreeMap não sincronizado podem causar sérios problemas de desempenho
- Para evitar esse tipo de problema, recomenda-se sincronizar o
TreeMap ou usar uma coleção thread-safe, como ConcurrentMap
1 comentários
Comentários do Hacker News
Eu achava que race conditions causavam corrupção de dados ou deadlocks, mas não tinha pensado que também poderiam causar problemas de desempenho. Os dados podem ser corrompidos de uma forma que gera um loop infinito
Collections.synchronizedMapou migrar paraConcurrentHashMape ordenar quando necessárioEm código com várias threads em execução, a única estratégia realmente confiável é tornar todos os objetos imutáveis e limitar os que não podem ser imutáveis a seções pequenas, autocontidas e rigidamente controladas
A menção de que "quase não dava para acessar via ssh" me lembrou uma situação da pós-graduação, quando eu usava um Sun UltraSparc 170
O código pode ser reduzido simplesmente ao seguinte
treeMap.putquando <i>unrelatedObjects</i> não está vazio. Isso pode ser um bugOutra forma de obter um loop infinito é usar uma implementação de <i>Comparator</i> ou <i>Comparable</i> que não implemente uma ordem total consistente
Pode-se considerar detectar ciclos usando um contador crescente e lançar uma exceção se ele ultrapassar a profundidade da árvore ou o tamanho da coleção
Em Java, executar operações concorrentes sobre objetos que não são thread-safe produz os bugs mais interessantes
Há a pergunta sobre se um TreeMap desprotegido pode causar 3.200% de utilização
O autor descobriu um tipo de Poison Pill. Isso é mais comum em sistemas de event sourcing: uma mensagem que mata tudo o que encontra
Exceções em threads são um problema absoluto
select()e threads lançando exceções