Il y a 4 semaines -

Temps de lecture 23 minutes

Asynchronisme en Java : passé, présent et future de la plateforme (partie 2)

Dans mon article pr écédent, nous avons découvert les principales fonctionnalités proposées par Java depuis la première version du langage jusqu’à la version 7. Focalisons-nous sur des versions récentes de Java :

Java 8 : la programmation fonctionnelle fait « coucou »

Comme vous le savez, la version 8 a généré une révolution au niveau du langage Java. Cette nouvelle version a implementé beaucoup d’idées des langages fonctionnels comme Scala, par exemple :

  • Classe Optional qui permet aux développeurs de créer du code plus auto-documenté en évitant le billion dollar mistake lié à la mauvaise utilisation de la valeur null
  • API Streams : opérations lazy et opérateurs map, flatmap et compagnie

En ce qui concerne les tâches en arrière plan, de nouvelles fonctionnalités ont été ajoutées.

CompletableFuture

En Java 8, une nouvelle classe fut introduite, appelée CompletableFuture. Cette classe vient compléter Future avec de la logique supplémentaire. Cette classe permet, parmi d’autres choses, d’enchaîner des opérations asynchrones d’une manière simple en suivant une approche plus fonctionnelle. Jetons un coup d’œil à la définition de cette classe :

public class CompletableFuture<T> extends Object implements Future<T>, CompletionStage<T> {
}

Comme vous pouvez le constater, cette classe implémente l’interface Future. Ce qui permet, encore une fois grâce à la magie de la POO, de faire évoluer une bibliothèque qui utilise l’interface Future sans impacter ses clients.

Si vous jetez un coup d’œil à la page javadoc de cette classe, vous vous rendrez compte qu’il s’agit d’une classe très complexe qui propose pas mal de méthodes. Par conséquent, je voudrais mettre l’accent sur certaines d’entre elles :

join()

On peut dire que get() est à Future ce que join() est à CompletableFuture. Autrement dit, join() permet de récupérer la valeur wrappée par un CompletableFuture. Étant donné que CompletableFuture implémente Future, pourquoi a-t-on besoin d’une nouvelle fonction ? La raison principale est la gestion des exceptions. Examinons plus en détail la signature des fonctions get() sans et avec timeout :

public V get() throws InterruptedException, ExecutionException
public T get​(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException

Comme vous pouvez le constater, plusieurs exceptions sont déclarées dans leurs signatures. En plus, il s’agit d’exceptions dites checked. Par conséquent, pour chaque appel get(), on doit encapsuler l’appel dans un bloc try-catch ou alors gérer l’exception dans un niveau supérieur de notre application.

Regardons maintenant la documentation de la fonction join() :

public T join()

Returns the result value when complete, or throws an (unchecked) exception if completed exceptionally. To better conform with the use of common functional forms, if a computation involved in the completion of this CompletableFuture threw an exception, this method throws an (unchecked) CompletionException with the underlying exception as its cause.

En effet, cette fonction lève une exception dite unchecked si une erreur se produit. Par conséquent, on n’a pas besoin de truffer notre code de try-catch et on peut utiliser une syntaxe plus propre et plus fluent. Pour finir, à l’image de la fonction Future.get() et ses variantes, la fonction CompletableFuture.join()bloque le thread courant.

runAsync()

Cette factory method permet de créer une instance de CompletableFuture à partir d’un Runnable. Cette fonction est idéale pour lancer des traitements asynchrones où on n’a pas besoin de récupérer une valeur à la fin.

supplyAsync()

On peut créer un CompletableFuture à partir d’un Supplier. Voici un exemple :

CompletableFuture<Integer> completable = CompletableFuture.supplyAsync(() -> longComputation());

thenCombineAsync()

CompletableFuture permet de composer un nouveau CompletableFuture à partir des deux CompletableFuture indépendants. Voici un exemple de code :

CompletableFuture&lt;String&gt; helloCompletable = CompletableFuture.supplyAsync(() -&gt; "Hello");
CompletableFuture<String> worldCompletable = CompletableFuture.supplyAsync(() -> "World");

CompletableFuture<String> result = helloCompletable.thenCombineAsync(worldCompletable, (hello, world) -> hello + " " + world);

String resultString = result.join();

Dans cet exemple, on combine les deux CompletableFuture grâce à la fonction thenCombineAsync. A la fin, la variable resultString contiendra le chaîne de caractères Hello World. Rien d’original.

thenApplyAsync()

Permet de lancer un traitement asynchrone à partir d’un autre CompletableFuture. Ce nouveau traitement sera lancé lorsque le premier CompletableFuture sera complété.

CompletableFuture&lt;Integer&gt; completable = CompletableFuture.supplyAsync(() -&gt; longComputation())
        .thenApplyAsync(i -&gt; i * 2);

Dans cet exemple de code, la fonction lambda qui multiplie la valeur par 2 ne sera planifiée dans l’ExecutorService sélectionné que lorsque la fonction longComputation, aussi planifiée sur l’ExecutorService sera complétée. Si vous êtes à l’aise avec l’API Streams de Java, cette fonction est équivalente à un map asynchrone.

allOf() / anyOf()

Lorsqu’on a présenté les défauts de l’interface Future, on avait dit qu’on ne peut pas attendre qu‘une série de Future soit complétée sans faire de l’active polling. Ceci peut être fait avec les fonctions allOf() et anyOf() de CompletableFuture

ExecutorService executor = Executors.newCachedThreadPool();

List<CompletableFuture<Integer>> futures = Stream.of(0, 1, 2, 3)
        .map(index -> CompletableFuture.supplyAsync(() -> longComputation(false)))
        .collect(Collectors.toList());

System.out.println("Waiting for completion");
CompletableFuture<Void> result = CompletableFuture.allOf(futures.toArray(new CompletableFuture[]{}));
result.join();
System.out.println("Done");

Dans ce code, on construit une liste de CompletableFuture pour ensuite appeler la fonction CompletableFuture.allOf(). Comme vous pouvez le remarquer, cette fonction accepte en paramètre un array de CompletableFuture et elle renvoie un CompletableFuture. Ce CompletableFuture nous permet de faire un join() pour bloquer le thread courant jusqu’à que tous les CompletableFuture seront complétés. Je pense qu’on est tous d’accord, cette solution est bien plus élégante que celle proposé par Future

Parallels Streams

Comme vous le savez, Java 8 a intégré l’interface Streams qui permet d’ajouter du mojo fonctionnel à Java en ce qui concerne le traitement des collections. On peut voir un Stream comme une séquence finie d’éléments du type T que l’on peut manipuler à l’aide d’une syntaxe déclarative. La conséquence la plus évidente de ce changement, c’est que cela permet d’augmenter l’expressivité de notre code en adoptant le lexique des langages fonctionnels. Néanmoins, je voudrais mettre l’accent sur un changement de paradigme moins évident à première vue.

Lorsqu’on voulait itérer sur une collection en Java 7, on avait 2 options :

  • Utiliser iterator() ou listIterator() (si disponible)
  • Utiliser une boucle for-each (utilise iterator()), while, etc.

Dans les deux cas, il s’agit d’une itération dite externe, cela veux dire que c’est au développeur de concevoir le code nécessaire pour accomplir l’itération. Streams propose un mode d’itération dit interne, c’est-à-dire, l’itération est contrôlée par la JVM et le développeur doit se concentrer uniquement sur les transformations à faire sur la collection. Ceci permet à la JVM de faire des optimisations sous le capot de manière transparente pour les développeurs. La fonctionnalité qui nous intéresse le plus par rapport à cet article est les streams parallèles. Prenons cet exemple :

Stream.iterate(1L, i -&gt; i + 1)
  .limit(50)
  .parallel()
  .reduce(0L, Long::sum);

Ignorons pour le moment l’appel à parallel(). Qu’est-ce que ce code fait ? Il génère un Stream contenant les 50 premiers nombres, pour ensuite les additionner via une opération de reduce. L’appel à parallel() implique :

  • Le stream est divisé en morceaux grâce à l’interface Spliterator. Si vous souhaitez en savoir plus, je vous invite à lire cet article de mon collège Joaquim Rousseau
  • Pour chaque morceau, une tâche est soumise dans le ForkJoinPool
  • Chaque tâche est traitée par un thread worker, comme nous l’avons évoqué dans la section consacrée au framework Fork/Join
  • On combine au fur est à mesure les résultats partiels des morceaux qui ont été déjà traités

Vu qu’on utilise Fork/Join pool, on n’a pas la garantie que les différents morceaux seront traités dans l’ordre, par conséquent, ce peut être une limitation pour intégrer cette approche dans notre code. Dans ce cas particulier, ceci n’a pas d’importance car :

  • L’opération « addition » est associative et commutative
  • Le traitement de chaque morceau est indépendant des autres morceaux

Observations par rapport à la performance

Comme on l’a vu tout à l’heure, transformer un stream séquentiel en parallèle est trivial, cependant ce n’est pas toujours une bonne idée. L’utilisation de ce type de Streams créé un surcoût par rapport aux Streams séquentiels, dû à l’utilisation de ForkJoinPool. Cependant, cela s’avère utile lorsqu’on doit traiter beaucoup d’éléments ou quand le traitement de chaque élément est coûteux. Dans la bibliographie, on trouve souvent la formule suivant: N * Q = 10000 où :

  • N : Nombre d’éléments à traiter
  • Q : Coût de traitement de chaque élément

Voici un lien intéressant sur StackOverflow à ce sujet: https://stackoverflow.com/questions/20375176/should-i-always-use-a-parallel-stream-when-possible

L’un des avantages des Streams est qu’ils permettent d’itérer facilement sur des collections mais, ces dernières ne proposent pas les mêmes performances lors d’une itération. Par exemple, tous les objets qui s’appuient sur des Arrays sont facilement divisibles car tous les éléments sont contigus en mémoire. Néanmoins, dans le cas des listes, on doit les traverser pour récupérer tous les éléments. Voici un tableau pour bien choisir son type de collection :

Java 9 : Reactive Streams: One ring to rule them all!

Comme vous pouvez le constater, l’introduction de la classe CompletableFuture a été une belle évolution par rapport à Future. Cependant elle reste une classe difficile à utiliser car, parmi d’autre choses, les noms choisis ne sont pas très bons. C’est pour cela, qu’une nouvelle approche était nécessaire.

There are only two hard things in Computer Science: cache invalidation and naming things.

-- Phil Karlton

Pour cette raison, plusieurs libraires tierces ont vu la lumière du jour. Voici quelques exemples :

  • RxJava : implémentation en Java des Reactives Extensions. Cette bibliothèque s’appuie sur le pattern Observer et elle est maintenue principalement par Netflix
  • Akka Streams : utilise le modèle d’Acteurs

Ces bibliothèques ont gagné beaucoup d’attention au fil du temps. Cependant, elles ont évolué de manière indépendante, ce qui rendait leur interopérabilité très difficile. C’est à ce moment où l’initiative Avengers Reactive Streams est née. Son but ? Proposer un cadre de travail commun pour les bibliothèques réactives afin de rendre plus facile leur cohabitation. Ce standard se base sur le pattern Publisher-Subscriber qui permet une communication entre émetteurs de données Publishers et récepteurs de données Subscribers d’une manière faiblement couplée. Pour ce faire, Java 9 propose quelques interfaces que les fournisseurs compatibles doivent implémenter. Suite à la publication de ce standard, les bibliothèques déjà existantes s’y sont adaptées, par exemple RxJava est compatible avec ce standard depuis la version 2. Les plus anciens se souviendront d’un cas similaire qui s’est produit avec la parution de Hibernate et la publication ultérieure de la spécification JPA. Toutes ces interfaces peuvent être trouvées dans la classe Flow.

Avant de commenter plus en détail toutes les interfaces, voici un diagramme de séquence qui explique tout ce processus de manière simple :

 

Publisher

Publisher est une interface fonctionnelle qui permet a un subscriber de s’inscrire lui-même afin de commencer à recevoir des événements produits par le publisher. Lorsqu’un subscriber appelle cette méthode, il indique de manière explicite qu’il veut recevoir dorénavant tous les événements produits par ce publisher. À partir de ce moment, le subscriber est manipulé par le publisher via les callbacks définis dans l’interface Subscriber

Subscriber

Dans cette interface, on peut trouver les 4 callbacks utilisées par le publisher pour notifier des événements à ses subscribers :

  • onSubscribe : Ce callback est immédiatement invoqué par le publisher après une souscription réussie. Le but ? Partager avec le subscriber une instance de l’interface Subscription. Cette instance est fondamentale pour le fonctionnement de toute l’API reactive streams, car elle permet d’implémenter le mécanisme de backpressure. Nous allons approfondir sur ce sujet plus tard.
  • onComplete : Callback invoqué par le publisher quand il a fini d’envoyer tous les événements. Dans le cadre d’un Stream infini, cette méthode n’est jamais invoquée.
  • onError : Callback invoqué par le publisher lorsqu’une exception a été levée lors du traitement. Cet événement est particulier car il s’agit d’un événement terminal. Autrement dit, aucun nouvel événement ne sera envoyé par le publisher à partir de celui-ci
  • onNext : Callback invoqué par le publisher afin d’envoyer un nouvel événement au subscriber

Subscription

On arrive au joyau de la couronne, l’interface Subscription ! Comme vous avez pu le constater, dès l’instant où un subscriber appelle la méthode subscribe d’un publisher, c’est ce dernier qui prend le contrôle. C’est lui qui envoie les événements en appelant les différents callbacks proposés par Subscriber tandis que le subscriber joue un rôle plutôt passif. La communication entre publisher-subscriber serait-elle unidirectionnelle ? Pas du tout ! La réponse est dans cette interface :

public interface Subscription {
  void request(long n);
  void cancel();
}

Comme nous l’avons déjà vu, une instance de cette classe est créée par le publisher lorsqu’un subscriber demande une souscription. Cette instance permet au subscriber de remonter des informations au publisher de manière découplée. Étant donné que le publisher et le subscriber peuvent être exécutés sur différents threads, il se peut que le publisher produise des événements plus rapidement que le subscriber n’est capable d’en consommer. Ceci est dangereux car il peut surcharger le subscriber. Pour éviter ceci, le subscriber indique de manière explicite au publisher le nombre maximum d’évènements qu’il est capable de traiter en appelant la méthode request(n) de l’objet subscription. Le publisher, de son côté, s’engage à ne pas envoyer plus d’événements qu’indiqué par le subscriber. Ce mécanisme est connu sous le nom de backpressure.

Évidemment, la méthode cancel() sert au subscriber pour notifier le publisher qu’il n’est plus intéressé par ses événements. À partir de ce moment, le publisher n’enverra plus d’événement à ce subscriber. On dira qu’il s’agit d’une rupture amicale.

Processor<T, R>

Toutes les interfaces présentées jusqu’à l’heure permettent de mettre en relation un publisher avec plusieurs subscribers d’une manière découplée. Dans cette communication, on a un publisher qui produit des événements et des subscribers qui réagissent à ces événements. Deux rôles bien définis et bien distincts. L’interface Processor<T, R>, par contre, permet de définir des objets qui se comportent en publishers et en subscribers au même temps. Pourquoi faire ? Voici les raisons :

  • Transformer les événements dans un pipeline réactif
  • Réagir aux événements terminaux (upstream) et proposer des alternatives : retry ? valeur par défaut ?

Attendez une minute ! Si l’article s’intitule « asynchronisme en Java », pourquoi on n’a parlé ni de threads ni d’ExecutorService ni de ForkJoinPool dans cette section ? Le standard Reactive Streams n’impose aucun modèle d’asynchronisme et délègue cette partie aux différentes implémentations. Ceci accorde aux implémentations plus de liberté pour gérer les threads et propose aux développeurs des outils différents et innovants. À ce jour, les implémentations les plus connues sont: RxJava et Project Reactor. Voyons un cas d’utilisation réel :

Cas d’utilisation : Project Reactor

Project Reactor est une implémentation du standard Reactive Streams portée par Pivotal, la société derrière la galaxie Spring. Reactor propose deux types réactifs : Flux et Mono. Flux est utilisé pour des séquences de n éléments (potentiellement infinis) et Mono pour les séquences de 0 ou 1 éléments. Ces deux types implémentent l’interface Publisher de Reactive Streams. Vous pouvez trouver plus d’information sur la documentation de référence officielle.

Ces types peuvent être manipulés via des operators comme map, flatMap, etc. afin de créer des pipelines. Le cycle de vie des pipelines réactifs a trois phases :

Assemblage : À l’image de l’API Streams de Java, Reactor propose une API fluent qui permet de manipuler nos données en entrée via des operators. À la fin de cette phase, le publisher ne fait rien et il attend la souscription d’un subscriber pour commencer à émettre des événements. De même que les Streams Java, les pipelines Reactor sont lazy.

Souscription : Cette phase est initiée par un subscriber lorsqu’il souscrit à un publisher. C’est à ce moment que le publisher commence à envoyer des événements. Nothing happens until subscribe.

Runtime : Les signaux entre le publisher et le subscriber sont échangés

Voici un petit schéma qui explique les trois phases :

Dans la documentation de Reactor, cette bibliothèque est décrite comme concurrency-agnostic. Pour ce faire, cette bibliothèque se sert des Schedulers et des opérateurs publishOn et subscribeOn.

Scheduler

On peut considérer les Schedulers de Reactor comme un ExecutorService sous stéroïdes. Aux fonctionnalités de base d’un ExecutorService, un Scheduler propose un timer interne qui permet manipuler le temps dans le cadre des tests, par exemple :

StepVerifier.withVirtualTime(() -&gt; Flux.range(1, 4).delayElements(Duration.ofHours(5)))
        .expectSubscription()
        .thenAwait(Duration.ofDays(1))
        .expectNextCount(4)
        .verifyComplete();

Comme vous pouvez le constater, on a créé un Flux qui émet les nombres entre 1 et 4 avec un délai entre chaque élément de 5 heures. Le fait d’avoir un timer virtuel nous permet de tester de manière instantanée ce genre de scénarios. Pour plus d’information concernant les Schedulers de Reactor, vous pouvez consulter la documentation officielle

publishOn

Les transformations faites après cet opérateur seront exécutées par un thread déterminé par le Scheduler utilisé. Voyons un exemple de code :

private Integer intenseCalculation(String str) {
    sleep(300); // Sleep for 300 ms
    return str.length();
}

Flux&lt;Integer&gt; input = Flux.just("Hello", "world!")
        .doOnNext(s -> System.out.printf("Before first map: Content=%s, Thread=%s\n", s, Thread.currentThread().getName()))
        .map(String::toUpperCase)
        .doOnNext(s -> System.out.printf("Before publishOn: Content=%s, Thread=%s\n", s, Thread.currentThread().getName()))
        .publishOn(Schedulers.parallel())
        .doOnNext(s -> System.out.printf("After publishOn: Content=%s, Thread=%s\n", s, Thread.currentThread().getName()))
        .map(this::intenseCalculation);

input.subscribe(length -> System.out.printf("Subscriber1: Content=%d, Thread=%s\n", length, Thread.currentThread().getName()));

Voici la sortie :

Before first map: Content=Hello, Thread=Test worker
Before publishOn: Content=HELLO, Thread=Test worker
Before first map: Content=world!, Thread=Test worker
After publishOn: Content=HELLO, Thread=parallel-1
Before publishOn: Content=WORLD!, Thread=Test worker
Subscriber1: Content=5, Thread=parallel-1
After publishOn: Content=WORLD!, Thread=parallel-1
Subscriber1: Content=6, Thread=parallel-1

Comme vous pouvez le constater, le pipeline est exécuté dans le thread principal nommé worker jusqu’à ce qu’on arrive à l’opérateur publishOn. À partir de ce moment, toutes les autres opérations sont exécutées sur un thread different parallel-1.

subscribeOn

Modifions l’exemple précédent pour utiliser subscribeOn au lieu de publishOn :

Flux<Integer> input = Flux.just("Hello", "world!")
        .doOnNext(s -> System.out.printf("Before first map: Content=%s, Thread=%s\n", s, Thread.currentThread().getName()))
        .map(String::toUpperCase)
        .doOnNext(s -> System.out.printf("Before subscribeOn: Content=%s, Thread=%s\n", s, Thread.currentThread().getName()))
        .subscribeOn(Schedulers.parallel())
        .doOnNext(s -> System.out.printf("After subscribeOn: Content=%s, Thread=%s\n", s, Thread.currentThread().getName()))
        .map(this::intenseCalculation);

input.subscribe(length -> System.out.printf("Subscriber1: Content=%d, Thread=%s\n", length, Thread.currentThread().getName()));

Voici la sortie :

Before first map: Content=Hello, Thread=parallel-1
Before publishOn: Content=HELLO, Thread=parallel-1
After publishOn: Content=HELLO, Thread=parallel-1
Subscriber1: Content=5, Thread=parallel-1
Before first map: Content=world!, Thread=parallel-1
Before subscribeOn: Content=WORLD!, Thread=parallel-1
After subscribeOn: Content=WORLD!, Thread=parallel-1

Comme vous pouvez le constater, l’utilisation de l’opérateur subscribeOn fait que tout le pipeline est exécuté dans un thread différent du thread principal.

Bien entendu, on n’a fait que gratter la surface car les possibilités de Reactor sont vastes. Si vous avez envie d’approfondir sur ce sujet, je vous invite à voir le talk Flight of the Flux de Simon Baslé, Software Engineer chez Pivotal.

Futur de la plateforme: Project Loom

Dans ces deux articles consacrés à l’asynchronisme en Java, nous avons présentés les différents solutions proposées par le langage pour lancer des opérations asynchrones sur la JVM. Voici quelques réflexions :

  • Le code synchrone nécessite moins d’effort pour comprendre son fonctionnement. Ceci permet de détecter plus facilement des défauts dans le code
  • Les outils de déboggage, profiling et test sont plus matures pour la programmation synchrone. Dans la plupart des cas, lorsqu’on a une exception dans le mode reactive, l’information contenue dans la trace n’est pas facilement exploitable
  • L’interaction avec du code legacy n’est pas naturelle lorsqu’on utilise les solutions asynchrones

Comme on dit qu’une image vaut mille mots, je me suis permis d’ajouter ce slide du talk Project Loom : Fibers and Continuations for Java de Alan Bateman

Arrivés à ce point, il faut se poser LA question : Vu la complexité de la programmation asynchrone et ses inconvénients, est-ce que ça vaut le coup d’adopter ce paradigme ? Les architectes de Java se sont posé la même question et ils ont commencé à travailler sur la prochaine révolution de l’asynchronisme en Java. Ce projet a été baptisé Projet Loom. Il a pour but de mettre à disposition des développeurs des outils pour faire de l’asynchronisme en utilisant une syntaxe synchrone. Sacré défi ! Comme d’habitude, les architectes ont pris des idées des autres langages.

DISCLAIMER : À ce jour, ce projet est en incubation et il n’y a pas de date de sortie prévue. Par conséquent, on ne verra pas de code. Cependant, on peut discuter des idées principales derrière ce projet.

Ce projet s’appuie sur plusieurs concepts :

Continuations

Les continuations constituent la base du projet Loom et on peut les voir comme des instances de Runnable qui peuvent être arrêtées et redémarrées par un Scheduler. Ces continuations sont exécutées par des threads et le choix de quelle continuation est exécutée est réalisé par un scheduler. Lorsqu’une continuation bloque pour une opération I/O, elle peux céder sa place à une autre continuation qui est prête à être exécutée. D’autres langages ont déjà proposé de solutions similaires comme Kotlin et ses coroutines.

Fibers : green threads are cool again

Dans les premières versions de la JVM, les threads étaient schedulés par la JVM elle même sans dépendre du système d’exploitation sous-jacent. Ces threads s’appellaient green thread ou user threads. Au fil de versions, cette approche a été abandonnée au profit de l’utilisation de threads natifs. Les raisons ? Les voici :

  • Les green threads ne pouvaient pas accéder à toutes les fonctionnalités multi-core des processeurs modernes. Par exemple, on ne peut pas scheduler deux threads sur deux cœurs différents. Ceci était très pénalisant lorsqu’un thread lançait une opération I/O synchrone. Cet appel bloquait TOUS les threads liés à ce processus.
  • L’implémentation initiale avait des problèmes concernant la synchronisation de threads

Cependant, ils présentaient plusieurs avantages par rapport aux threads natifs liés au fait qu’on ne devait pas passer par le système d’exploitation :

  • La création et la synchronisation étaient plus rapides que celles des threads natifs. Dans ce dernier cas, on a besoin de faire des appels système, ce qui est cher en ce qui concerne la performance.
  • Leur empreinte mémoire était plus faible

À partir de la version 1.2, il faut savoir que, lorsque la JVM crée un thread, elle crée un mapping 1:1 avec un thread système. Autrement dit, une instance de la classe Thread est une abstraction d’un thread natif. Bien que l’approche des green threads a été vite abandonnée, on peut toujours repérer leur existence dans la classe Thread https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/doc-files/threadPrimitiveDeprecation.html

Ce dont on a besoin est d’un système léger de création de thread gérés par la JVM. Dans le projet Loom ce concept s’appelle Fiber. Une Fiber est composée d’une Continuation et un Scheduler (ForkJoinPool par défaut).

Tail-call optimization

Technique héritée des langages fonctionnels qui permet de diminuer la mémoire réservée pour le stack lorsque l’on utilise la récursivité. À ce jour, aucune action a été faite à ce sujet, mais vous pouvez en apprendre un peu plus sur ce sujet pour votre culture personnelle dans cette page sur StackOverflow

Conclusion

Dans cette série d’articles, nous avons parcouru ensemble les principaux mécanismes que Java nous propose pour ajouter de la « sauce asynchrone » dans notre code. Comme vous avez pu constater, de plus en plus d’outils ont été ajoutés afin de rendre cette fonctionnalité plus simple à mettre en œuvre. Malgré cette évolution manifeste, certains pensent que le langage Java stagne car son évolution est bien plus lente que celle d’autres langages qui orbitent autour de la JVM. Ils ont raison… en partie. Le 23 mai 2020, Java a fêté ses 25 ans et l’un des piliers qui ont marqué le devenir de ce langage est la rétrocompatibilité avec les versions précédentes du langage. Ceci a généré du passif qui empêche au langage d’évoluer plus rapidement. Dans le monde de la technologie, cette situation a été présentée sous le nom de l’innovators dilemma :

  • L’évolution du langage Java se fait par des petits incréments, afin de ne pas casser l’existant et de ne pas perturber les utilisateurs
  • D’autres langages comme Kotlin ou Scala peuvent se permettre d’évoluer plus rapidement. Ceci leur permet de proposer des fonctionnalités plus innovantes (ou disruptives)

Dans le livre indispensable Modern Java in action, on décrit l’interaction entre les langages de la JVM et Java d’une manière très originale : en le comparant avec le fameux effet serre impliqué dans le changement climatique. Voici une illustration incluse dans le livre :

Les langages « de niche » expérimentent avec des idées nouvelles qui sont ensuite portées sur les langages majoritaires, dont Java fait partie. La symbiose appliquée aux langages de programmation, n’est-ce pas beau ?

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.