Nos cambiamos a hilos virtuales de Java 21 y obtuvimos un interbloqueo en TPC-C para Postgres

Los nuevos hilos virtuales de Java 21 están sacando a la luz riesgos sutiles de interbloqueo en código Java existente, especialmente cuando primitivas de sincronización antiguas como `synchronized` y `Object.wait()` se usan dentro de bibliotecas como los pools de conexiones JDBC. Los comentaristas señalan que estos constructos pueden “pinnear” los hilos portadores y agotar el pool subyacente limitado, rompiendo programas que eran seguros con muchos hilos OS. El consenso es que los hilos virtuales son prometedores para cargas intensivas en E/S, pero todavía no son una sustitución directa en todas partes, y que los desarrolladores deberían auditar bibliotecas, preferir utilidades de concurrencia modernas (locks, semáforos, colecciones concurrentes) y ajustar o esperar correcciones del runtime antes de una adopción amplia.

Naturaleza del interbloqueo

  • Hay consenso en que los hilos virtuales no “crearon mágicamente” una nueva clase de interbloqueo; el benchmark puso de manifiesto un problema de diseño existente bajo nuevas restricciones.
  • El interbloqueo surgió porque los hilos virtuales quedaron bloqueados dentro del código del pool de conexiones c3p0 usando synchronized + Object.wait, agotando los carrier threads.
  • La solución del artículo —proteger las conexiones a la BD con un Semaphore— evita que los hilos virtuales entren en c3p0 hasta que haya una conexión disponible, de modo que se bloqueen en código consciente de los VT en lugar de dentro del pool.
  • Varios comentaristas subrayan que se trata de un problema genérico de JDBC/pool de conexiones, no específico de PostgreSQL.

Hilos virtuales, pinning y progreso hacia adelante

  • Los hilos virtuales no pueden desmontarse cuando:
    • Se ejecutan dentro de bloques/métodos synchronized, o
    • Ejecutan código nativo/externo, o ciertas llamadas bloqueantes heredadas como Object.wait.
  • En estos casos, el hilo portador (OS) queda “pinneado”; si ocurren muchas de estas llamadas, el pool de carrier threads del programador de VT (por defecto máximo 256, configurable mediante jdk.virtualThreadScheduler.maxPoolSize) puede agotarse.
  • Algunos aclaran que Object.wait tanto libera el monitor como está implementado en código nativo, así que el problema no es solo synchronized, sino también que wait en sí no coopera con la planificación de VT.
  • La preocupación no es tanto la corrección de los locks, sino la pérdida de “progreso hacia adelante” cuando todos los carriers están bloqueados.

“Sustitución directa” vs. nuevas semánticas

  • La documentación y algunos mensajes presentaron los hilos virtuales como algo casi intercambiable; otros señalan la guía de adopción, que advierte explícitamente contra cambiar ciegamente todos los hilos de plataforma por VT.
  • Un bando: si tu diseño requiere más hilos ejecutables simultáneamente que carriers, el diseño era frágil; VT solo lo pone en evidencia.
  • El otro bando: el código anterior era correcto bajo la suposición de que el SO siempre podía crear más hilos; introducir un límite duro de carriers cambia silenciosamente el comportamiento del sistema y puede romper ese código.
  • Hay desacuerdo sobre si la documentación advirtió adecuadamente sobre interbloqueos frente a meros problemas de “escalabilidad”.

Soluciones alternativas y buenas prácticas discutidas

  • Sustituir patrones synchronized/wait/notify por:
    • Bloqueos, semáforos y conditions de java.util.concurrent.
    • Colas concurrentes para patrones productor–consumidor.
  • Evitar operaciones largas o bloqueantes dentro de synchronized; considerarlo mala práctica incluso sin VT.
  • Usar pools de carrier threads y configuración:
    • Aumentar jdk.virtualThreadScheduler.maxPoolSize cuando sea apropiado.
    • Descargar llamadas CPU-bound o nativas a ejecutores de hilos de plataforma.
  • Usar herramientas de diagnóstico como -Djdk.tracePinnedThreads para encontrar zonas problemáticas.
  • Algunos sugieren cambiar pools antiguos como c3p0 por otros mantenidos activamente (p. ej., HikariCP), aunque incluso los pools modernos siguen adaptándose a los VT.

Madurez de los hilos virtuales y futuras correcciones

  • Muchos consideran que los hilos virtuales son potentes, pero aún no son de “configúralo y olvídate”; funcionan mejor para código moderno, intensivo en E/S, que evita primitivas heredadas.
  • Varios señalan que el comportamiento de synchronized/wait es una limitación de implementación conocida y temporal, y que el equipo del JDK está trabajando para hacerlos compatibles con VT.
  • Algunos recomiendan esperar a un futuro LTS en el que synchronized y Object.wait cooperen plenamente con los hilos virtuales antes de adoptarlos ampliamente, especialmente en bases de código con muchas bibliotecas de terceros.

Comparaciones y metadiscusión

  • Se trazan comparaciones con Go, Erlang, Node.js, Python async y el pool de hilos/async de .NET: todos tuvieron rarezas iniciales al mezclar modelos bloqueantes y no bloqueantes.
  • Otras discusiones cuestionan si el threading verde/M:N puede llegar a ser realmente transparente en ecosistemas construidos alrededor de hilos del SO.
  • Menores desvíos tratan sobre el editorialismo del título en HN y el rechazo generalizado a las imágenes hero generadas por IA en publicaciones técnicas.