Les nouveautés de Java 19 et 20
La version 19 de Java datant de septembre 2021 apporte de nouvelles fonctionnalités telles qu’une mise à jour du pattern matching pour instanceof, introduit en feature preview de Java 14. Mais également des améliorations pour la gestion de la mémoire, ainsi que des améliorations pour la sécurité.
De plus cette version inclut quelques «?petites?» feature previews comme le projet Loom et ses 99 468 lignes de code réparties dans 1133 fichiers modifiés. Il a pour but de remplacer les threads actuels présents depuis la création de Java grâce à un innovant système de virtual threads.

La dernière version de Java, sortie en mars 2023, est Java 20. Cette version apporte de nouvelles fonctionnalités en feature preview comme les scoped values, les record patterns, le pattern matching for switch, les foreign functions et la memory API, mais également des améliorations liées au projet Loom comme les virtual threads et les structured concurrency.
Nous allons, dans cet article, et via des exemples concrets, vous parler de la combinaison virtual threads et structured concurrency, qui composent à eux deux le projet Loom introduit dans Java 19.
Le projet Loom : qu’est-ce que c’est ?
Avant d’entrer dans le détail voici un petit rappel des différentes évolutions liées à la programmation concurrente ou asynchrone en Java :
Java 1.0 – 1995 : Thread, Runnable, synchronized()
Java 5 – 2004 : ExecutorService, Callable, locks
Java 8 – 2014 : Fork / Join, parallel streams, CompletableFuture
La programmation concurrente est utilisée principalement dans deux cas de figure :
- l’exécution de traitements en parallèle pour utiliser tous les cœurs du CPU ;
- l’augmentation du débit de traitement d’une application pour traiter plus de requêtes.
Cependant, l’un des plus gros problèmes existant depuis leur création est?qu’une fois une tâche lancée par un thread, elle ne peut plus être arrêtée correctement et il faut attendre soit?sa complétion, soit une interruption via une exception.
Par ailleurs, un thread prend 1ms pour se lancer.
Par exemple :
- si on en lance 1 000, cela va prendre 1 seconde ;
- si on en lance 1 000 000, cela va prendre 1 000 secondes (soit un peu plus d’un quart d’heure).
Un thread passe donc 99,99 % du temps à attendre et il faudrait 1 million de threads en simultané pour occuper 100 % du CPU. Malheureusement un thread coûte cher en ressources.
En effet, la taille d’un thread Java est fixe. Elle est définie par défaut à 1 024 KB sur une JVM 64-bit et peut être définie grâce à l’option « -Xss ». De ce fait, la JVM aura un nombre maximum de threads allouable qui dépendra donc de la configuration de la machine.
Par exemple sur un PC Windows avec 32 Go de RAM avec le JDK 19 on peut voir ci-dessous que nous avons 16 825 864 KB « Reserved* 1 » et 728 584 KB en « Commited** 2 » pour les threads de la JVM.
Nous pourrons donc allouer jusqu’à 16 431 threads simultanément en sachant qu’à peu près 712 sont déjà alloués et utilisés par la JVM.

Reserved* : la taille garantie pour être accessible par l’OS hôte (mais pas encore allouée ni utilisable par la JVM).
Commited** : taille déjà prise, accessible et allouée par la JVM.
Pour pallier ce problème, la programmation asynchrone est arrivée et permet de lancer plusieurs traitements sur un même thread.
Dans l’exemple ci-dessous, nous utilisons des CompletableFuture pour simuler les opérations asynchrones. La méthode processOrders crée une liste de Future pour effectuer les différentes étapes de traitement des commandes, puis attend leur complétion avant de collecter les résultats finaux.

Il existe également une librairie « Project Reactor » venant de la communauté de Spring et qui profite de l’avantage d’être basée sur les APIs Java (CompletableFuture, Stream, Duration, ExecutorService, Flow, etc.) mais utilise aussi le design pattern Observable. Cette librairie est notamment utilisée par Spring WebFlux et permet le développement d’API Rest Reactive.
Cependant cette manière d’utiliser les threads est plutôt difficile à écrire, à lire, à tester et à « debugger » et donc compliquée à maintenir.
C’est là qu’intervient le projet Loom en proposant de revenir à une programmation synchrone, plus simple, MAIS d’augmenter le nombre possible de threads en simultané via l’utilisation de «?Virtual Threads?» qui consomment environ 1 000 fois moins de ressources qu’un thread classique. Il est alors possible de créer jusqu’à 1 million de virtual threads sur une machine. Ces virtual threads sont gérés directement par la JVM, et donc l’allocation de ces threads ne nécessite pas d’appels au système d’exploitation de la machine.
Grâce à Loom on peut s’affranchir des pools de thread, les virtual threads sont si légers (environ 1 000 fois plus qu’un thread classique, de l’ordre du kilo-octet), que cela nous permet de les laisser mourir sans intervention et d’en utiliser d’autres si besoin.

Et niveau code ça donne quoi ?
Concernant le développement dans votre code Java, la class VirtualThread n’est pas visible. On utilisera donc toujours la class Thread. Mais on utilisera une implémentation qui va créer le code :

Si vous êtes adepte des ExecutorService, vous pouvez les utiliser directement et simplement en utilisant la méthode suivante :

De ce fait si vous utilisiez déjà des threads et/ou des ExecutorServices dans certaines de vos applications, le passage vers les virtual threads pourrait s’effectuer avec très peu d’adaptations.
Attention cependant, les ExecutorServices ne tuent pas les threads qui n’ont pas terminé leur exécution. C’est là où on peut utiliser directement les try with resources en déclarant notre scope avec ES.ShutdownOnSuccess. Une fois l’exécution du scope effectuée, l’ensemble des virtual threads restant sont stoppés.

Étant donné que le projet Loom est encore en « preview » il faudra au préalable activer cette feature de Java grâce à l’option : –-enable-preview

Il est possible avec quelques milliers de threads de pouvoir les analyser via son IDE ou bien via des dumps de thread via JMC (Java Mission Control) par exemple.
En revanche avec le million de threads permis par les virtual threads cela devient mission impossible. C’est pour cela qu’a été introduite dans Java 20 la notion de structured concurency, ou pattern de programmation concurrente structurée.
Mais comment fonctionne la programmation concurrente structurée ?
L’objectif est de regrouper un ensemble de threads, on parle ici de « scope » qui est un pool de thread structuré en arbre. Lorsqu’un thread fils génère de nouveaux ensembles de threads, ces ensembles deviennent des sous-groupes rattachés au pool principal.
La structuration de chaque scope sera donc le reflet du scope métier que vous voulez implémenter.
Pour pouvoir utiliser les structured concurency introduits par Java 20 il faudra en plus de l’option —enable-preview ajouter l’option —add-modules=jdk.incubator.concurrent
Dans notre exemple nous avons voulu représenter un service de réservation de salle de sport. Ce service se chargera d’appeler différentes salles de sport pour connaitre leurs programmes, et choisira celle qui correspondra à nos critères de recherche. Par exemple la salle dont la date de la prochaine session est la plus proche.

Pour ajouter du traitement métier au sein d’un scope, on peut le créer directement soi-même en implémentant la méthode handleComplete de l’ExecutorService au sein de notre class fille.

Liez ce code avec des fork/join au sein de chaque scope et votre code sera synchrone, bien plus lisible, testable et maintenable qu’avec un code de type asynchrone.
Conclusion :
À travers cet article, nous avons pu découvrir et comprendre le fonctionnement des virtual threads et ce qu’ils apporteront à la programmation asynchrone.
Lors de la rédaction de cet article, Java 21 future LTS, qui sortira en septembre 2023, a annoncé le changement de statut des virtual threads, passant de « Proposed to Target » à « Targeted ». Ils seront donc présents comme feature à part entière à côté des autres features de Java 21 : pattern matching for switch, record patterns, sequenced collections, etc.
Le code utilisé pour nos tests est présent sur notre github : https://github.com/ekit3/Loom-jdk20
Sources :
https://www.proxiad.com/Loom-le-futur-de-Java-programmation-asynchrone/
https://univalence.io/blog/articles/project-Loom-les-continuations-et-les-thread-virtuels/
https://www.invivoo.com/projet-Loom-thread-virtuel/
https://dzone.com/articles/how-much-memory-does-a-Java-thread-take