2 pontos por GN⁺ 2025-03-01 | 1 comentários | Compartilhar no WhatsApp
  • 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

 
GN⁺ 2025-03-01
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

    • Acho que erros, comportamento anômalo ou avisos em um projeto devem, por princípio, ser corrigidos. Isso porque podem causar problemas aparentemente não relacionados
    • É bem conhecido que as coleções centrais do Java não são thread-safe por design. O OP deve verificar se várias threads também estão manipulando coleções em outras partes do código
    • A solução mais fácil é envolver o TreeMap com Collections.synchronizedMap ou migrar para ConcurrentHashMap e ordenar quando necessário
    • Operações individuais no map podem ser tornadas thread-safe, mas não dá para ter certeza de que uma sequência de operações seja thread-safe. Não dá para ter certeza de que o objeto que possui o TreeMap seja thread-safe
    • Como solução discutível, rastrear os nós visitados não é uma boa abordagem. A coleção continua não sendo thread-safe e ainda pode falhar de outras formas sutis
    • Um desenvolvedor atento aos detalhes poderia perceber a combinação de threads com TreeMap, ou sugerir não usar TreeMap se elementos ordenados não forem necessários. Mas neste caso isso não aconteceu
    • O problema é a violação do contrato da coleção, e trocar TreeMap por HashMap ainda continuaria errado
  • Em 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

    • Reescrevi módulos centrais seguindo esse princípio, e eles passaram de fonte constante de problemas a uma das partes mais resilientes da base de código
    • Com essas diretrizes, o code review ficou muito mais fácil
  • 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

    • Um novo usuário ou aluno tentou executar trabalho em paralelo, dividindo um arquivo de texto grande em várias seções por número de linha e processando cada seção em paralelo
    • Muita RAM foi consumida, e as tentativas de swap ficavam buscando freneticamente para ler diferentes seções do mesmo arquivo
    • Não era possível obter um prompt de login no console, mas já havia uma sessão autenticada e foi possível conseguir uma sessão root para resolver o problema
    • O problema foi não entender os limites do sistema
  • O código pode ser reduzido simplesmente ao seguinte

    • O código original só faz treeMap.put quando <i>unrelatedObjects</i> não está vazio. Isso pode ser um bug
    • É preciso verificar se <i>a</i> e <i>b</i> retornam o mesmo valor todas as vezes, e se <i>treeMap</i> está se comportando como um map
  • Outra 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

    • Isso não tem relação com concorrência e pode acontecer dependendo de dados específicos e da ordem de processamento
  • 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

    • Isso praticamente não exige overhead de memória ou CPU e tende a ser mais aceitável
  • 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

    • Vi um problema semelhante por volta de 2009, e isso ainda pode acontecer
    • É decepcionante para quem acha que data races são só um pouco ruins
  • O autor descobriu um tipo de Poison Pill. Isso é mais comum em sistemas de event sourcing: uma mensagem que mata tudo o que encontra

    • Quando a estrutura de dados chega a um estado ilegal, todas as threads subsequentes ficam presas na mesma bomba lógica
  • Exceções em threads são um problema absoluto

    • Há histórias de caça a bugs aterrorizantes envolvendo C++, select() e threads lançando exceções