Devoxxx 2026 - Le projet Loom
Réécrire la concurrence dans Java, c'est un peu comme refaire les fondations d'un immeuble sans en évacuer les habitants. C'est exactement ce défi colossal qu'ont décrypté José Paumard et Rémi Forax lors de leur Deep Dive sur le Projet Loom à Devoxx France cette année.
On va décrypter les deux piliers particulièrement importants pour nos applications, que ce soit en termes de performance ou de sécurité: les Virtual Threads (final en Java 21) et les Scoped Values (final en Java 25).
Bref, commençons par le commencement, pourquoi est-ce qu'on a eu besoin des Virtual Threads ?
Un TL;DR est diponible à la fin de l'article
Le problème des Thread actuels
Le problème historique ? Un java.lang.Thread c'est simplement un wrapper sur un thread système (Kernel).
C’est lourd : ~2 Mo de mémoire et ~1 ms pour démarrer. Résultat, pour une application qui doit rester "légère" où on ne voudrait pas dépasser 3 Go de mémoire, on est déjà bloqué à ~1250 Threads simultanés si on laisse un peu de place pour le GC et nos objets métiers. Pour une API avec beaucoup de trafic, on sera rapidement limité.
C'est bloquant: ce modèle, lorsqu’une requête effectue un appel I/O (accès disque, appel API externe ou requête SQL), le thread reste bloqué. Il ne travaille plus, mais il conserve ses 2 Mo de RAM et continue de monopoliser une ressource précieuse du système d’exploitation. On se retrouve avec des serveurs qui affichent un CPU à 10% mais qui sont incapables d'accepter une nouvelle connexion car la mémoire est saturée par des threads qui... ne font rien à part attendre.
Ce modèle de Platform Thread a été réglé par des langages tels que Kotlin ou Go qui utilisent des green Threads ou encore coroutines.
Peut-on utiliser ses Threads à 100%?
Oui! Java propose une solution avecCompletabeFuturepour faire de la programmation réactive. Si vous ne connaissez pas l'API, voici un exemple comparé à un ExecutorService:
ExecutorService es = ...;
var f1 = es.submit(someService::readImages);
var f2 = es.submit(someService::readLinks);
var page = new Page(f1.get(1, TimeUnit.SECONDS),
f2.get(1, TimeUnit.SECONDS));
---
var cf1 = CompletableFuture
.supplyAsync(someService::readImages)
.whenComplete((images, error) -> {
if (error != null) {
// log something
}
});
var cf2 = // same with readLinks
var cf3 = cf1.runAfterBoth(cf2, () -> {
var page = new Page(cf1.get(), cf2.get());
// do something with page
});
Java
Comme le dit bien José Paumard pendant la conférence: "C'est terrible à écrire, à debugger et à profiler". Les premiers à avoir industrialisé ce modèle de programmation en Java sont Netflix avec RxJava. Si vous voulez plus de détails, ils expliquent leurs problématiques et leur implémentation dans cet article.
Les CompletableFutures se basent sur des enchaînements de lambdas non bloquantes (donc sur des callnio), notre thread se retrouve à exécuter une quantité astronomique de lambdas qui peuvent s'exécuter en ~100 ns.
Le problème: Le moindre appel bloquant, de seulement 100 ms (un call API par exemple), bloque l'exécution d'1 million de tasks. Et on n'a plus l'impression d'écrire du Java (peut-être du javascript...), autant écrire du elixir 💧.
C'est ainsi qu'en 2017 le projet Loom est né avec une ambition: régler ce problème des Threads système.
Le projet Loom
Le projet Loom vient proposer un modèle plus simple (exactement comme les Thread classic) mais avec les performances de la programmation réactive en gardant de parfaites capacités de debug et de profiling.
Le saviez-vous ? Loom, c'est presque 100 000 lignes de code ajoutées au JDK pour rendre ce mécanisme transparent.
Virtual Threads : Plus légers que l'air
Une image vaut mieux que mille mots, alors voici l'explication en image:

Voici un pool de 3 Threads (de type ForkJoinPool) qui va exécuter un ensemble de VT (tous les rectangles de couleur).

Chaque Thread prend une tâche dans sa file d'attente.

Lorsqu'une task rencontre une opération bloquante, le worker l'envoie dans la Heap, le contexte d'exécution (la stack) de la VT est compressé pour plus tard, on obtient un code asynchrone. On comprend alors que tout notre code actuel bloquant peut DÉJÀ tourner dans des VT sans aucune modification, merci Loom!

Ensuite les tâches d'après sont exécutées et les Threads qui n'en ont plus peuvent faire ce qu'on appelle du Work Stealing, ils volent le travail des autres Threads.

Enfin pour revenir à notre VT qui était dans la Heap, le kernel signale que l'I/O est fini, la data est prête à être processée. Le contexte qui était dans la Heap revient dans la file pour être exécuté par un worker exactement là où il s'était arrêté.
La JVM devient un chef d'orchestre capable de jongler avec des millions de tasks sur seulement quelques threads.
La véritable prouesse de Loom est de rendre l'asynchrone invisible. Plus besoin de frameworks réactifs complexes ou de callbacks illisibles : vous écrivez du code séquentiel simple, et la JVM s'occupe de le rendre ultra-performant sous le capot. C'est le retour à la simplicité, sans compromis sur la puissance. Mais les VT apportent d'autres problématiques, notamment le problème du "pinning" avec les synchronized .
Le Thread Pinning, rend ce Thread!
Pour faire simple, lorsque l'on utilise un synchronized sur un objet pour du code multithreadé, la JVM vient écrire un Lock Record dans la stack et écrit l'adresse de ce Lock Record dans les headers de l'objet synchronized. Or on a dit que la stack des Virtual Threads peut être déplacée dans la Heap lors d'un I/O, si on fait là notre objet synchronisé va être corrompu... C'est ce qu'on appelle du pinning, le worker est bloqué avec un VT à cause du synchronized .

Netflix s'est aussi confronté à ce problème, qu'ils ont extrêmement bien expliqué dans cet article: Java 21, Virtual Thread, Dude Where's my lock. Si vous avez des synchronized vous pouvez utiliser des ReentrantLock afin d'éviter ces hard locks. Ou alors vous pouvez passer en Java24+ où ce problème de pointeur interne à la stack est déplacé vers la Heap et c'est grâce à un autre projet: le projet Liliput.
Conclusion
Il est essentiel de garder en tête qu'un thread virtuel n'est rien d'autre qu'un objet Java classique, géré par la JVM plutôt que par le système d'exploitation. Cette nature légère permet au moteur de Loom de réaliser son tour de magie : bloquer un thread virtuel ne bloque jamais son thread porteur (carrier thread), libérant ainsi les ressources physiques pour d'autres tâches. De plus, les VT, ce n'est pas seulement pour les performances, mais surtout pour "le modèle de programmation et l'observabilité": tu codes de manière classique!
Cet article ne couvre qu'un tiers de cette merveilleuse conférence. Une deuxième partie est sûrement à suivre, que ce soit sur les gains réels que l'on peut obtenir avec les VT ou sur les StructuredTaskScope.
TL;DR
Le projet Loom répond à une limitation historique de Java : les threads classiques (java.lang.Thread) sont de simples wrappers sur des threads système, lourds (~2 Mo par thread, ~1 ms de démarrage) et bloquants lors des opérations I/O. Cela conduit à des serveurs avec un CPU à 10% mais une mémoire saturée par des threads qui n'attendent que des réponses.La programmation réactive (via CompletableFuture) offrait une alternative plus performante, mais au prix d'un code complexe, difficile à debugger et à profiler. C'est pour réconcilier performance et lisibilité que Loom a été lancé en 2017.Les Virtual Threads (finalisés en Java 21) sont des objets Java légers gérés par la JVM, et non par l'OS. Lors d'un blocage I/O, leur contexte d'exécution est compressé et déplacé dans la Heap, libérant le thread porteur (carrier thread) pour d'autres tâches. La JVM peut ainsi orchestrer des millions de tâches sur seulement quelques threads réels.Un écueil notable est le "pinning" : l'utilisation de synchronized peut bloquer le thread porteur. La solution recommandée est d'utiliser ReentrantLock, ou de passer à Java 24+ qui résout ce problème grâce au projet Lilliput.En résumé, les Virtual Threads permettent d'écrire du code séquentiel simple tout en bénéficiant de performances comparables à la programmation réactive, sans en subir la complexité.