JEP 444, Virtual Threads

Contexte

Les threads virtuels ont été introduits dans le JDK 19 en mode aperçu, améliorés dans le JDK 20. Ils sont maintenant intégrés en standard. Cela correspond à une proposition incluant les travaux du projet Loom

Le thread au niveau de la JVM sont basés sur les threads systèmes. C’est à dire que c’est le système qui s’occupe de planifier leur execution. Cela implique aussi que le nombre de thread est limité car cela nécessite des ressources systèmes.

Objectif

L’objectif est d’avoir un thread léger qui permettent d’avoir des milliers et des millions de thread afin de réaliser des traitements concurrents.

Les threads classiques s’appellent maintenant les threads plateformes ("a plateform thread") afin de les différencier des threads virtuels.

Ce qui est intéressant, c’est que les threads virtuels héritent de la même classe java.lang.Thread. En revanche, il n’y a pas de constructeurs. En effet, la mode est de passer de plus en plus par des fabriques de méthodes. Pour cela, nous avons la méthode statique : Thread.startVirtualThread()

Le thread est démarré avec une instance de l’interface java.lang.Runnable. Ainsi, nous pouvons écrire la classe suivante :

class HelloRunnable implements Runnable {
    public void run() {
        System.out.println(Thread.currentThread() + " dit Coucou");
    }
}

Utilisation

Nous pouvons tester les threads virtuels avec la commande jshell.

bin/jshell

Nous allons commcer par déclarer la classe HelloRunnable cité ci-dessus. Ensuite, nous allons pouvoir créer notre instance.

var inst = new HelloRunnable();

Il nous reste à démarrer le thread virtuel.

Thread.startVirtualThread(inst);

A noter, que nous pouvons utiliser la méthode Thread.ofVirtual() qui utilise le pattern Builder

Thread.ofVirtual().start(inst);

Cette méthode offre deux avantages :

  • Nous avons une méthode similaire pour les threads plateformes

Thread.ofPlatform().start(inst);
  • Nous pouvons créer une instance sans démarrer le thread (ou en tout cas sans le démarrer tout de suite.)

Thread courant = Thread.ofVirtual.unstarted(inst);
courant.start();

Pour rappel, Java 8 est passé par là avec les lambas. Donc nous pouvons écrire tout simplement :

Thread.ofVirtual().start(() -> {
    System.out.println(Thread.currentThread() + " dit Coucou");
});

Remarques

En exécutant le code ci-dessus, nous obtenons la sortie suivante

VirtualThread[#25]/runnable@ForkJoinPool-1-worker-1 dit Coucou
$1 ==> VirtualThread[#25]/runnable

Le #25 est une sorte de compteur de thread virtuel. Si nous exécutons de nouveau la même commande, nous obtiendrons la sortie suivante

VirtualThread[#26]/runnable@ForkJoinPool-1-worker-1 dit Coucou
$1 ==> VirtualThread[#26]/runnable

C’est normal car nous lançons un nouveau thread virtuel. Ce qui veut dire que la première fois, il y a eu 24 thread virtuels exécutés avant notre premier appel.

astuceQui les utilisent ? La JVM elle-même.

Compléments sur l’API

  • Il est possible de savoir si nous sommes dans un thread virtuel ou non. Nous avons la méthode Thread.isVirtual() qui renvoie un booléen.

  • Les méthodes Thread.join() et Thread.sleep() possède une surcharge prenant en paramètre une instance de Duration.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(java.time.Duration.ofSeconds(1));
            System.out.println(" " + i + " : " + Thread.currentThread());
            return i;
        });
    });
}

astuceA noter que dans l’exemple précédent, nous venons de créer 10 000 threads virtuels.

  • La méthode Thread.getId() est dépréciée. Il faut passer par la méthode Thread.threadId(). Ce dernier renvoie un identifiant.

avertissementLa raison est que la méthode getId() n’est pas une méthode finale donc il est possible de la surcharger.

  • La méthode Thread.getAllStackTraces() retourne une map de toutes les threads plateforme et non tous les threads comme son nom le laisse penser.

astuceCela s’explique car nous pouvons avoir des millions de threads virtuels donc il n’y a pas de sens de retourner une map aussi grosse.

Différence sur l’API

  • Les threads virtuels sont forcément des threads daemon. La méthode Thread.setDaemon() n’a pas d’effet.

avertissementPensez à cela pour les conditions d’arrêt de la JVM. En effet, il faut au moins un thread plateforme sinon la JVM s’arrête.

  • Dans le même style, la priorité est fixée à Thread.NORM_PRIORITY. La méthode Thread.setPriority() n’a pas d’effet.

  • Les méthodes stop(), suspend() et resume() lèvent une exception lors de leur invocation si c’est un thread virtuel.

avertissementPour rappel, ces méthodes sont déjà dépréciés au moins depuis le JDK 6.

  • Dans le même sens de l’histoire, il n’y a pas de permissions si le SecurityManager est activé.

avertissementPour rappel, ce dernier est déprécié pour suppression depuis le JDK 17. Plus d’informations, dans ce billet JDK 17 Nouveautés

Nouveautés

Compatible avec les variables Thread-local

Les threads virtuels supportent maintenant les variables thread-local (ThreadLocal).

Il faudra néanmoins faire attention. Le support est présent uniquement pour faciliter la migration et le support des librairies vers les threads virtuels. C’est à dire que cela reste à éviter au maximum vis à vis des contraintes.

avertissementCe qu’il faut absolument éviter, c’est de faire une réserve ("un pool") de thread.

Le mieux est d’utiliser les "Scoped valued" JEP 429, mais cela est une autre JEP.

astuceUne propriété système jdk.traceVirtualThreadLocals permet de tracer toute utilisation de ThreadLocal dans les threads virtuels. Par défaut, la valeur est false.

Concurrence

Là encore, pour faciliter la transition, notamment avec l’API des Executeurs, il existe maintenant Executors.newVirtualThreadPerTaskExecutor() qui permet d’avoir un thread virtuel par tâche.