Java 19 : quoi de neuf ?

Java 19 : quoi de neuf ?

Maintenant que Java 19 est features complete (Rampdown Phase One au jour d’écriture de l’article), c’est le moment de faire le tour des fonctionnalités qu’apporte cette nouvelle version, à nous, les développeurs.

Cet article fait partie d’une suite d’article sur les nouveautés des dernières versions de Java, pour ceux qui voudraient les lire en voici les liens : Java 18, Java 17, Java 16, Java 15, Java 14, Java 13, Java 12, Java 11Java 10, et Java 9.

JEP 405 : Record Patterns (Preview)

Cette JEP vise à enrichir le pattern matching de Java avec les record patterns qui permettent de déconstruire un record en ses attributs.

Un record est un nouveau type dont le but est d’être un conteneur de donnée, il a été introduit en Java 14.

Avant la JEP 405, si vous vouliez faire du pattern matching sur un record puis accéder à ses attributs, vous auriez écrit du code comme celui-ci :

record Point(int x, int y) {}

static void printSum(Object o) {
    if (o instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x + y);
    }
}

La JEP 405 permet de déconstruire le record et de directement accéder à ses attributs quand le pattern match, ce qui simplifie le code comme suit :

record Point(int x, int y) {}

void printSum(Object o) {
    if (o instanceof Point(int x, int y)) {
        System.out.println(x + y);
    }
}

Au lieu, dans le bloc de code exécuté au match du pattern, d’avoir accès à la variable Point p; vous avez accès directement aux attribut int x, int y du record Point.

C’est la première étape dans la déconstruction de type en Java, on peut espérer voir prochainement la déconstruction arriver pour toutes les classes et pas uniquement les records.

Plus d’information dans la JEP 405.

JEP 422 : Linux/RISC-V Port

RISC-V est une architecture de jeu d’instructions (Instruction Set Architecture – ISA) RISC (Reduced Instruction Set Computer) gratuite et open source conçue à l’origine à l’Université de Californie à Berkeley, et maintenant développée en collaboration sous le parrainage de RISC-V International : https://riscv.org.

RISC-V définit plusieurs ISA, seul le jeu d’instruction RV64GV (general purpose 64bit) a été porté.

Ce port comprend le support du JIT (C1 et C2) ainsi que de tous les GC existant (y compris ZGZ et Shenandoah).

Plus d’information dans la JEP 422.

JEP 425 : Virtual Threads (Preview)

Virtual threads are lightweight threads that dramatically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications.

La JEP 425 introduit en preview feature les Virtual Threads (parfois aussi nommés green threads ou lightweight threads), ce sont des threads légers, avec un coût de création et de scheduling faible, qui permettent de faciliter l’écriture d’application concurrente.

C’est le projet Loom d’OpenJDK.

Les threads Java classiques sont implémentés via des threads OS, quel est le problème avec cette implémentation de thread ?

  • Créer un thread en Java implique de créer thread OS et donc un appel système, ce qui est couteux.
  • Un thread a une stack de taille fixe, par défaut de 1Mo sur Linux amd64, configurable via -Xss ou -XX:ThreadStackSize.
  • Un thread sera schedulé via le scheduler de l’OS, chaque changement d’exécution d’un thread entraînera donc un context switch qui est une opération couteuse.

Pour palier à ces problèmes, les applications concurrentes ont eu recours à plusieurs types de construction :

  • Les pools de threads qui permettent de réutiliser un thread pour plusieurs requêtes (HTTP, JDBC, …). Le degré de concurrence de l’application est donc la taille du pool.
  • La programmation réactive qui va ouvrir un nombre de threads très réduit (1 ou 2 par unité de CPU), et se baser sur un système d’event loop pour traiter un ensemble de requêtes concurrentes sur ces quelques threads.

Mais ces constructions impliquent de la complexité lors du développement des applications. L’idée des Virtual Threads est d’offrir des threads peu couteux à créer, et donc de permettre de se passer de pool de threads ou d’event loop et d’utiliser un thread pour chaque requête. C’est la promesse du projet Loom.

Le principe est de proposer des threads virtuels qui sont créés et schedulés par la JVM, et utilisent un pool de thread OS, ce que l’on appelle des carrier threads (thread porteur). La JVM va alors automatiquement monter et démonter les threads virtuels des carrier threads quand nécessaire (lors des I/O par exemple). Les stacks des threads seront stockées dans la heap de la JVM, leur taille n’étant plus fixe, un gain en mémoire est possible.

La manière la plus simple de créer des threads virtuels est via Executors.newVirtualThreadPerTaskExecutor(), on a alors un ExecutorService qui exécutera chaque tâche dans un nouveau thread virtuel.

Là où les threads virtuels sont le plus intéressant est pour les applications qui nécessitent une grande concurrence (plusieurs dizaines de milliers de thread) et/ou qui ne sont pas CPU bound (généralement celles exécutant des I/O). Attention, utiliser des threads virtuels pour des applications CPU bound, des applications faisant des calculs intense par exemple, est contre-productif, car avoir plus de threads que de CPU pour ce type d’application impact négativement les performances.

Il existe des limitations dans l’implémentation actuelle des threads virtuels, dans certains cas un thread virtuel va épingler (pinning) le carrier thread qui ne pourra pas traiter d’autres threads virtuels :

  • L’utilisation du mot clé synchronized
  • L’utilisation de méthode native ou de l’API Foreign Function de la JEP 424

À cause de ces limitations, les threads virtuels ne sont pas forcément la solution à tous les problèmes de concurrence, même si des améliorations pourrons être faites dans les prochaines version de Java. Il faudra aussi que l’écosystème devienne compatible avec les threads virtuels (par exemple en évitant l’utilisation de bloc synchronisé).

Plus d’information dans la JEP 425.

JEP 428 : Structured Concurrency (Incubator)

Simplify multithreaded programming by introducing a library for structured concurrency. Structured concurrency treats multiple tasks running in different threads as a single unit of work, thereby streamlining error handling and cancellation, improving reliability, and enhancing observability.

Incubator module qui offre une nouvelle API permettant de simplifier l’écriture de code multi-threadé en permettant de traiter plusieurs tâches concurrentes comme une unité de traitement unique.

Même si les Virtual Threads sont les stars de cette nouvelle version de Java, l’API Structured Concurrency est pour moi aussi intéressante, si ce n’est plus, en tout cas pour un développeur, car va impacter fortement la manière d’écrire du code multi-threadé / concurrent.

Le but de cette API est de pouvoir écrire un code multi-threadé plus lisible et avec moins de risque d’erreur dans son implémentation via une meilleure gestion des erreurs.

Avant la Structured Concurrency API, pour exécuter un ensemble de tâches concurrentes, puis joindre les résultats ; on écrivait un code comme celui-ci (exemple pris de la JEP).

ExecutorService ex = Executors.newFixedThreadPool(nbCores);
Response handle() throws ExecutionException, InterruptedException {
    Future<String>  user  = es.submit(() -> findUser());
    Future<Integer> order = es.submit(() -> fetchOrder());
    String theUser  = user.get();   // Join findUser 
    int    theOrder = order.get();  // Join fetchOrder
    return new Response(theUser, theOrder);
}

Il y a plusieurs problèmes ici :

  • Si une erreur arrive dans la méthode fetchOrder(), on va quand même attendre la fin de la tâche findUser()
  • Si une erreur arrive dans la méthode findUser(), alors la méthode handle() va se terminer, mais le thread qui exécute fetchOrder() va continuer à s’exécuter, c’est un thread leak.
  • Si la méthode handle() est interrompue, cette interruption n’est pas propagée aux sous-tâches et leurs threads vont continuer à s’exécuter, c’est un thread leak.

Avec la Structured Concurrency API, on peut utiliser la classe StructuredTaskScope qui permet de lancer un ensemble de tâches et de les gérer comme une unité de traitement unique. Le principe est de découper une tâche en sous tâche (via fork()) qui vont se terminer au même endroit : scope.join(). Voici l’exemple pris de la JEP.

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future<String>  user  = scope.fork(() -> findUser()); 
        Future<Integer> order = scope.fork(() -> fetchOrder());

        scope.join();          // Join both forks
        scope.throwIfFailed(); // ... and propagate errors

        // Here, both forks have succeeded, so compose their results
        return new Response(user.resultNow(), order.resultNow());
    }
}

Ici les deux sous-tâches findUser() et fetchOrder() sont démarrés dans leur propre thread (par défaut un thread virtuel), mais sont jointes et potentiellement annulées ensemble (comme une une unité de traitement). Leurs exceptions et leurs résultats seront agrégés dans la tâche parente, et toute interruption du thread parent sera propagé aux threads enfants. Lors d’un thread dump, on verra bien les sous-tâches comme enfants du thread parent, facilitant le debugging de code écrit de cette manière.

Plus d’information dans la JEP 428.

Les fonctionnalités qui restent en preview

Les fonctionnalités suivantes restent en preview (ou en incubator module).

Pour les détails sur celles-ci, vous pouvez vous référer à mes articles précédents.

  • JEP-426 – Vector API : quatrième incubation de la fonctionnalité. Cette nouvelle version s’intègre avec la Foreign Function & Memory API en permettant de lire/écrire un vector depuis un MemorySegments.
  • JEP-424 – Foreign Function & Memory API : après deux incubations pour ces deux fonctionnalités rassemblées dans une même JEP, celles-ci passent en preview.
  • JEP-427 – Pattern Matching for switch : troisième preview avec l’introduction de la clausewhen pour les guards à la place du &&.

Divers

Divers ajouts au JDK :

  • BigInteger.parallelMultiply() : identique à BigInteger.parallelMultiply() mais va utiliser un algorithme parallèle qui utilisera plus de ressources CPU et mémoire pour les grands nombres.
  • Integer et Long voient l’ajout des méthodes compress et expand
  • HashMap.newHashMap(int), HashSet.newHashSet(int), LinkedHashMap.newLinkedHashMap(int) et LinkedHashSet.newLinkedHashSet(int) : ces méthodes permettent de créer des maps ou des sets pouvant accueillir le nombre d’éléments passé en paramètre sans déclencher de resize.
  • Les constructeurs de la classe Locale ont été dépréciés, il faut maintenant construire une locale via Locale.of().

La totalité des nouvelles API du JDK 19 peuvent être trouvées dans The Java Version Almanac – New APIs in Java 19.

Des changements internes, de la performance, et de la sécurité

Chaque nouvelle version du JDK apporte ses optimisations de performances (entre autres GC et méthodes intrisics), et de sécurité. Celle-ci ne fait pas défaut.

Côté sécurité, le focus a été mis sur le renforcement de la sécurité de la JVM et les performance du TLS. Vous pouvez vous référer à l’article de Sean Mullan pour une liste exhaustive des changements de sécurité inclus cette version : JDK 19 Security Enhancements

Thomas Schatzl décrit les changements côté Garbage Collector inclus dans cette version dans son article JDK 19 G1/Parallel/Serial GC changes.

Conclusion

La tant attendue JEP 425 Virtual Threads est enfin arrivée et pourrait bien changer la donne dans la programmation concurrente en baissant le coût de création des threads en Java. En conjonction avec la JEP 428 qui permet d’écrire du code concurrent plus simplement et avec une meilleure gestion des erreurs, c’est une mini révolution qui se prépare ;).

Bien sûr, l’adoption des virtual threads se fera lentement, car l’écosystème devra se mettre à jour pour les supporter, mais c’est une étape importante pour Java et la JVM.

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.