Aller au contenu
Proxiad Blanc 2
REJOIGNEZ-NOUS
Rechercher
Fermer ce champ de recherche.

FR

|

EN

ARTICLE

Loom : le futur de Java et de la programmation asynchrone

Le projet Loom a pour objet d’apporter un nouveau modèle de programmation asynchrone dans le JDK. Une nouvelle notion de Thread arrive pour la plateforme Java : nos vieux threads d’il y a 25 ans laisseront la place à un nouveau type de thread, plus léger, et qui pourront être créés en plus grand nombre. Nous parlerons de programmation asynchrone, de programmation concurrent structurée, de scope et de scope local.

Résumé de la conférence donnée par José Paumard lors du devfest 2022

Avec la version Java 19 (Septembre 2022) est sorti en preview feature le projet Loom, l’un des plus gros projets actuels pour Java. Il a pour but de remplacer les threads actuels, présents depuis la création de Java, par un nouveau type de thread : les threads virtuels.

Avant de détailler l’implémentation de Loom, un petit historique sur la programmation concurrente et de ses limites s’impose. Pour rappel, différentes évolutions ont marqué la programmation concurrente en Java :

  • 1995 : Thread, Runnable, synchronized()
  • 2004 : ExecutorService, Callable, locks
  • 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 (ce qui nous intéresse ici dans le cadre d’une application web)

Cependant, un problème persiste depuis la création des threads : une fois une tâche lancée par un thread, elle ne peut plus être arrêtée correctement et on doit attendre soit sa complétion, soit une interruption via une exception (InterruptedException). Ainsi, une bonne gestion des lancements est primordiale pour un déroulement efficace de l’application.
Il y a dix ans, un thread était égal à une requête HTTP :

  • préparation de la requête : 10 ns
  • envoi de la requête et attente de la réponse : 10 ms
  • traitement de la réponse : 10 ns

Un thread passait donc 99.999% du temps à attendre. Il faudrait alors un million de threads en simultané pour occuper le CPU à 100%. Or, un thread coûte cher en ressources :

  • temps de lancement : ~1 ms
  • mémoire occupée : 2 Mo
  • temps pour passer d’un thread à l’autre : ~100 µs

Il est donc virtuellement impossible de lancer un million de threads classiques sur une machine (20 minutes de lancement, 2 To de mémoire). Pour tenter de pallier au problème, la programmation asynchrone a été introduite afin de pouvoir lancer plusieurs traitements sur le même thread. Cependant, cette manière d’utiliser les threads est beaucoup plus compliquée que la programmation synchrone, car elle est :

  • Difficile à écrire / lire, tester et debugger
  • Impossible à profiler

L’objectif du projet Loom était alors de revenir à la programmation synchrone (avec un thread par traitement), plus simple, mais d’augmenter le nombre possible de threads en simultané sur une machine.

Loom

La solution est « simple » : il suffit de raccourcir le temps de lancement et de limiter la mémoire occupée.

C’est exactement le but que Loom cherche à accomplir, en remplaçant les threads existants par des threads virtuels qui consomment beaucoup moins de ressources. Ainsi, un thread virtuel coûte ~1000 fois moins de ressources qu’un thread classique :

  • mémoire de l’ordre du Ko (au lieu de Mo)
  • création de l’ordre des µs (au lieu de ms)

Il est alors possible de créer un million de threads virtuels sur une machine.

Utilisation des threads virtuels

Pour créer un thread virtuel simple, on peut utiliser la classe VirtualThread, qui étend la classe Thread mais qui n’est pas visible en dehors de son package. Ainsi, on continue à utiliser l’objet Thread dans notre code.

Thread virtualThread = Thread.ofVirtual()
    .name("demoLoom")
    .start(runnable);

Il est également possible d’utiliser un ExecutorService de thread virtuel. L’ExecutorService d’un Tomcat ou d’un Jetty peut être alors remplacé pour que celui-ci utilise des threads virtuels.

ExecutorService service = Executors.newVirtualThreadPerTaskExecutor();

Structured Concurrency

Il est possible, avec quelques milliers de threads en simultané, de les analyser avec un IDE ou d’analyser des dumps de threads. Cependant, cela deviendrait impossible avec les millions de threads permis par Loom. Il a donc fallu structurer ces threads.

Le principe de Structured Concurrency a pour objectif de regrouper ensemble les threads. Un loom scope est un pool de threads structuré en arbre. Si un thread fils crée d’autres paquets de threads, ces paquets deviennent des enfants du pool principal.

Le nombre de scopes dépend ensuite de la logique métier implémentée derrière.

Exemple

loom

Une agence de voyage veut à la fois, contacter plusieurs serveurs météorologiques et retourner les résultats du premier service qui répond, et obtenir le meilleur résultat entre différents serveurs de devis.

Récupérer le résultat du premier thread ayant fini sa tâche

public static Weather getWeather() {
    try (var scope = new StructuredTaskScope.ShutdownOnSuccess<Weather>()) {
        Future<Weather> futureA = scope.fork(() -> getWeatherFromA());
        Future<Weather> futureB = scope.fork(() -> getWeatherFromB());
        Future<Weather> futureC = scope.fork(() -> getWeatherFromC());

        scope.join();

        System.out.println("future A = " + futureA.state())
        System.out.println("future B = " + futureB.state())
        System.out.println("future C = " + futureC.state())

        return scope.result();
    }
}

Ici, un scope (StructuredTaskScope.ShutdownOnSuccess) est créé pour gérer les threads qui récupèrent les données météorologiques.
Le scope ShutdownOnSucces est un scope spécifique qui s’arrête lorsque l’une des Futures créées par scope.fork() a retourné un résultat.

Si on regarde plus en détails l’état des trois Futures, dans le cas où getWeatherFromB() finit en premier, la sortie standard affiche le résultat suivant :

future A = FAILED
future B = SUCCESS
future C = FAILED
…

Récupérer le meilleur résultat parmi tous les threads

Concernant les devis (quotation), la première réponse ne suffit pas car l’agence de voyage souhaite obtenir la solution la moins chère des trois devis. Des scopes permettent d’analyser et de comparer les résultats des différents threads :

private static class QuotationScope extends StructuredTaskScope<Quotation> {

    private final Collection<Quotation> quotations = new ConcurrentLinkedQueue<>();

    private final Collection<Throwable> exceptions = new ConcurrentLinkedQueue<>();

    @Override
    protected void handleComplete(Future<Quotation> future) {
        switch(future.state()) {
            case RUNNING -> throw new IllegalStateException("Oops");
            case SUCCESS -> this.quotations.add(future.resultNow());
            case FAILED -> this.exceptions.add(future.exceptionNow());
            case CANCELLED -> {}
        }
    }

    public Quotation bestQuotation() {
        return this.quotations.stream()
        .min(Comparator.comparing(Quotation::quotation))
        .orElseThrow(this::exceptions);
    }

    public QuotationException exceptions() {
        QuotationException exception = new QuotationException();
        this.exceptions.forEach(exception::addSuppressed);
        return exception;
    }
}

Ici, la classe StructuredTaskScope a été étendue afin de redéfinir le traitement effectué lorsqu’un thread se termine. Pour cela, la méthode handleComplete() a été redéfinie et est lancée à chaque fois que l’un des threads du scope se termine. Le switch permet de définir les actions à exécuter en fonction de l’état du thread :

  • RUNNING : le thread a fini mais est encore en train d’exécuter un traitement. Ceci ne devrait pas se produire et une exception est donc lancée
  • SUCCESS : le thread a retourné un résultat. Nous ajoutons le devis trouvé à une liste. La liste est créée sur le thread global et peut être accédée par plusieurs threads, il faut donc veiller à utiliser une collection concurrente
  • FAILED : le thread a rencontré une erreur et a lancé une exception, qui est ajoutée dans une liste d’exceptions
  • CANCELLED : le thread a été créé mais annulé avant d’être lancé. Nous ne faisons donc rien dans ce cas

La méthode bestQuotation() a été créée pour retourner le meilleur prix ou lancer toutes les exceptions rencontrées si aucun devis n’a été trouvé.

Il ne reste alors plus qu’à lancer les threads qui appellent les différents serveurs, et d’attendre leur fin avec scope.join() pour récupérer le meilleur devis :

public static Quotation getQuotation() {
    try (var scope = new QuotationScope()) {
        Future<Quotation> futureA = scope.fork(() -> getQuotationFromA());
        Future<Quotation> futureB = scope.fork(() -> getQuotationFromB());
        Future<Quotation> futureC = scope.fork(() -> getQuotationFromC());

        scope.join();

        return scope.bestQuotation();
    }
}

Joindre les résultats des deux méthodes

Pour l’instant, les deux objectifs ont été définis séparément. La méthode suivante récupère parallèlement les résultats obtenus et les retourne :

public static TravelPage getTravelPage() {
    try (var scope = new TravelPageScope()) {
        scope.fork(Weather::getWeather);
        scope.fork(Quotation::getQuotation);

        scope.join();

        return scope.getTravelPage();
    }
}

Ainsi, les deux méthodes sont lancées dans un scope personnalisé (voir sur le github dans les sources). Ce scope récupère simplement les deux résultats et initialise un objet TravelPage qui pourra être récupéré grâce à la méthode getTravelPage().

Conclusion

Au cours de cet article, nous avons pu voir comment le projet Loom et l’introduction des threads virtuels vont faire évoluer les threads que l’on connaît depuis la création du langage en 1995. Le projet est encore au stade expérimental et ne sera proposé qu’en mode preview lors des JDK 19 et 20.

Je vous invite à visionner (lien dans les sources) la rediffusion des conférences du Devfest pour la démonstration en direct par José Paumard, et du Devoxx pour plus de détails (2h30).

Sources

  • Conférence de José Paumard à la Devfest 2022 : https://www.youtube.com/watch?v=jGJG6eUGQoo
  • Conférence de José Paumard et Rémi Forax lors de la Devoxx 2022 : https://www.youtube.com/watch?v=wx7t69DylsI
  • JEP 425 traitant de Loom : https://openjdk.org/jeps/425
  • JEP 428 traitant de la Structured Concurrency : https://openjdk.org/jeps/428

Direction Technique
Stéphane YVON
Proxiad NORD

  • Applicatif
  • Infrastructures
  • Cybersécurité
  • DevOps
  • Contact

PARIS

47 Rue de Ponthieu
75008 Paris
France
contact.idf@proxiad.com

LILLE

15 rue du Palais Rihour
59000 Lille
France
contact.nord@proxiad.com

ROUEN

4 Passage de la Luciline,
76000 Rouen
France
contact.normandie@proxiad.com

NANTES

275 Boulevard Marcel
Paul, 44800 Saint-
Herblain, France
contact.ouest@proxiad.com

STRASBOURG

3 Avenue de l'Europe,
67300 Schiltigheim
France
contact.est@proxiad.com

AIX-MARSEILLE

Europarc de Pichaury - 1330
av Guillibert de la Lauziére,
13290 Aix-en-Provence
contact.aixmarseille@proxiad.com

SOPHIA ANTIPOLIS

930 route des Dolines
06560 Valbonne
France
sophiantipolis@proxiad.com

LYON

170 Bd de Stalingrad
2e étage
69006 Lyon
contact.lyon@proxiad.com

BORDEAUX

Le Now Coworking,
Quai des Chartrons, Hangar 15
33300 Bordeaux
contact.bordeaux@proxiad.com

SOFIA

59 Boulevard G.M. Dimitrov,
1700 Sofia,
Bulgaria
contact.bulgaria@proxiad.com

PLOVDIV

6 Belgrad St,
4000 Plovdiv
Bulgaria
contact.bulgaria@proxiad.com

GREEN SI &
GREEN SI CONSULTING

47 rue de Ponthieu,
75008 Paris, France
contact.greensi@proxiad.com

SKOPJE

13 Maksim Gorki str, Nastel Business Center
1000 Skopje
Macédoine du Nord
contact.macedonia@proxiad.com

RENNES

801 Av. des Champs Blancs
35510 Cesson-Sévigné
contact.bretagne@proxiad.com

© 2025 PROXIAD

  • MENTIONS LéGALES
  • POLITIQUE DE CONFIDENTIALITé
  • siège social, 47 rue de ponthieu, 75008 paris, france
  • +33 1 44 83 83 70
Linkedin Instagram Youtube Facebook
Gérer le consentement aux cookies
Pour offrir les meilleures expériences, nous utilisons des technologies telles que les cookies pour stocker et/ou accéder aux informations des appareils. Le fait de consentir à ces technologies nous permettra de traiter des données telles que le comportement de navigation ou les ID uniques sur ce site. Le fait de ne pas consentir ou de retirer son consentement peut avoir un effet négatif sur certaines caractéristiques et fonctions.
Fonctionnel Toujours activé
Le stockage ou l’accès technique est strictement nécessaire dans la finalité d’intérêt légitime de permettre l’utilisation d’un service spécifique explicitement demandé par l’abonné ou l’utilisateur, ou dans le seul but d’effectuer la transmission d’une communication sur un réseau de communications électroniques.
Préférences
Le stockage ou l’accès technique est nécessaire dans la finalité d’intérêt légitime de stocker des préférences qui ne sont pas demandées par l’abonné ou l’utilisateur.
Statistiques
Le stockage ou l’accès technique qui est utilisé exclusivement à des fins statistiques. Le stockage ou l’accès technique qui est utilisé exclusivement dans des finalités statistiques anonymes. En l’absence d’une assignation à comparaître, d’une conformité volontaire de la part de votre fournisseur d’accès à internet ou d’enregistrements supplémentaires provenant d’une tierce partie, les informations stockées ou extraites à cette seule fin ne peuvent généralement pas être utilisées pour vous identifier.
Marketing
Le stockage ou l’accès technique est nécessaire pour créer des profils d’utilisateurs afin d’envoyer des publicités, ou pour suivre l’utilisateur sur un site web ou sur plusieurs sites web ayant des finalités marketing similaires.
Gérer les options Gérer les services Gérer les fournisseurs En savoir plus sur ces finalités
Voir les préférences
{title} {title} {title}
Logo Proxiad
  • Accueil
  • Expertises
  • Rejoignez-nous
  • Accueil
  • Expertises
  • Rejoignez-nous

NOUS REJOINDRE

CONTACTEZ-NOUS

SUIVEZ-NOUS

Linkedin Instagram Youtube Facebook
RAPPORT RSE