Mudamos para virtual threads do Java 21 e tivemos um deadlock em TPC-C para Postgres
As novas virtual threads do Java 21 estão expondo riscos sutis de deadlock em código Java existente, especialmente quando primitivas de sincronização antigas como `synchronized` e `Object.wait()` são usadas dentro de bibliotecas como pools de conexão JDBC. Comentários observam que esses construtos podem “pin”ar threads carrier e esgotar o pool limitado de threads subjacentes, quebrando programas que eram seguros com muitas OS threads. O consenso é que as virtual threads são promissoras para workloads intensivos em I/O, mas ainda não são uma substituição direta em todos os casos, e que os desenvolvedores devem auditar bibliotecas, preferir utilitários modernos de concorrência (locks, semáforos, coleções concorrentes) e ajustar ou aguardar correções do runtime antes de uma adoção ampla.
Natureza do deadlock
- Há consenso de que as virtual threads não criaram “magicamente” uma nova classe de deadlock; o benchmark expôs um problema de projeto existente sob novas restrições.
- O deadlock surgiu porque virtual threads ficaram bloqueadas dentro do código do pool de conexões
c3p0, usandosynchronized+Object.wait, esgotando as carrier threads. - A correção no artigo — proteger as conexões com o banco com um
Semaphore— impede que as virtual threads entrem no c3p0 até que uma conexão esteja disponível, então elas bloqueiam em código ciente de VT em vez de dentro do pool. - Vários comentaristas enfatizam que este é um problema genérico de JDBC/pool de conexões, não específico do PostgreSQL.
Virtual threads, pinning e progresso à frente
- Virtual threads não podem ser desmontadas quando:
- Estão executando dentro de blocos/métodos
synchronized, ou - Estão executando código nativo/estrangeiro, ou certas chamadas bloqueantes legadas como
Object.wait.
- Estão executando dentro de blocos/métodos
- Nesses casos, a thread “carrier” (OS) fica “pinada”; se muitas chamadas assim ocorrerem, o pool de carriers do agendador de VT (máximo padrão 256, configurável via
jdk.virtualThreadScheduler.maxPoolSize) pode ser esgotado. - Alguns esclarecem que
Object.waittanto libera o monitor quanto é implementado em código nativo, então o problema não é apenassynchronized, mas também o fato de o própriowaitnão cooperar com o agendamento de VT. - A preocupação é menos com a correção dos locks e mais com a perda de “progresso à frente” quando todos os carriers ficam bloqueados.
“Substituição direta” vs. novas semânticas
- A documentação e parte da comunicação apresentaram as virtual threads como, em grande parte, substituição direta; outros apontam o guia de adoção, que adverte explicitamente contra trocar cegamente todas as platform threads por VTs.
- Um lado: se seu design exige mais threads executáveis simultaneamente do que carriers, o design era frágil; VT apenas expõe isso.
- Outro lado: o código anterior estava correto sob a suposição de que o SO sempre poderia criar mais threads; introduzir um limite rígido de carriers muda silenciosamente o comportamento do sistema e pode quebrar esse código.
- Há discordância sobre se a documentação alertava adequadamente sobre deadlocks ou apenas sobre questões de “escalabilidade”.
Workarounds e boas práticas discutidas
- Substituir padrões
synchronized/wait/notifypor:- Locks, semáforos e conditions de
java.util.concurrent. - Filas concorrentes para padrões produtor–consumidor.
- Locks, semáforos e conditions de
- Evitar operações longas ou bloqueantes dentro de
synchronized; isso já é má prática mesmo sem VTs. - Usar pools de carrier threads e configuração:
- Aumentar
jdk.virtualThreadScheduler.maxPoolSizequando apropriado. - Descarregar chamadas CPU-bound ou nativas para executores com platform threads.
- Aumentar
- Usar ferramentas de diagnóstico como
-Djdk.tracePinnedThreadspara localizar áreas problemáticas. - Alguns sugerem trocar pools antigos como c3p0 por opções mantidas ativamente (por exemplo, HikariCP), embora até pools modernos ainda estejam se ajustando para VTs.
Maturidade das virtual threads e correções futuras
- Muitos veem as virtual threads como poderosas, mas ainda não “configure e esqueça”; elas são melhores para workloads de IO, código moderno e que evita primitivas legadas.
- Vários observam que o comportamento de
synchronized/waité uma limitação de implementação conhecida e temporária, e que a equipe do JDK está trabalhando para torná-las amigáveis a VT. - Alguns recomendam esperar por um futuro LTS em que
synchronizedeObject.waitcooperem totalmente com virtual threads antes de adotá-las amplamente, especialmente em codebases com muitas bibliotecas de terceiros.
Comparações e meta-discussão
- Foram feitas comparações com Go, Erlang, Node.js, Python async e o thread pool/async da .NET: todos tiveram peculiaridades iniciais ao misturar modelos bloqueantes e não bloqueantes.
- Discussões paralelas questionam se o green threading/M:N pode realmente ser totalmente transparente em ecossistemas construídos em torno de threads do SO.
- Outros desvios menores tratam da editorialização do título no HN e do desagrado generalizado com imagens heroicas obviamente geradas por IA em posts técnicos.