Différence entre Thread et Virtual Thread en Java

1. Introduction

Dans ce didacticiel, nous montrerons la différence entre les threads traditionnels en Java et les threads virtuels introduits dans Project Loom.

Ensuite, nous partagerons plusieurs cas d'utilisation pour les threads virtuels et les API que le projet a introduites.

Avant de commencer, nous devons noter que ce projet est en cours de développement actif. Nous allons exécuter nos exemples sur la machine virtuelle de métier à accès anticipé: openjdk-15-loom + 4-55_windows-x64_bin.

Les versions plus récentes des builds sont libres de modifier et d'annuler les API actuelles. Cela étant dit, il y a déjà eu un changement majeur dans l'API, car la classe java.lang.Fiber précédemment utilisée a été supprimée et remplacée par la nouvelle classe java.lang.VirtualThread .

2. Présentation de haut niveau de Thread par rapport à Virtual Thread

À un niveau élevé, un thread est géré et planifié par le système d'exploitation, tandis qu'un thread virtuel est géré et planifié par une machine virtuelle . Maintenant, pour créer un nouveau thread de noyau, nous devons faire un appel système, et c'est une opération coûteuse .

C'est pourquoi nous utilisons des pools de threads au lieu de réallouer et de désallouer les threads si nécessaire. Ensuite, si nous souhaitons faire évoluer notre application en ajoutant plus de threads, en raison du changement de contexte et de leur empreinte mémoire, le coût de maintenance de ces threads peut être important et affecter le temps de traitement.

Ensuite, généralement, nous ne voulons pas bloquer ces threads, ce qui entraîne l'utilisation d'API d'E / S non bloquantes et d'API asynchrones, ce qui pourrait encombrer notre code.

Au contraire, les threads virtuels sont gérés par la JVM . Par conséquent, leur allocation ne nécessite pas d'appel système et ils ne sont pas soumis au changement de contexte du système d'exploitation . En outre, les threads virtuels s'exécutent sur le thread porteur, qui est le thread noyau réel utilisé sous le capot. En conséquence, puisque nous sommes libres du changement de contexte du système, nous pourrions engendrer beaucoup plus de threads virtuels.

Ensuite, une propriété clé des threads virtuels est qu'ils ne bloquent pas notre thread de support. Avec cela, bloquer un thread virtuel devient une opération beaucoup moins chère, car la JVM planifiera un autre thread virtuel, laissant le thread porteur débloqué.

En fin de compte, nous n'aurions pas besoin de rechercher des API NIO ou Async. Cela devrait aboutir à un code plus lisible, plus facile à comprendre et à déboguer. Néanmoins, la continuation peut potentiellement bloquer un thread porteur - en particulier, lorsqu'un thread appelle une méthode native et effectue des opérations de blocage à partir de là.

3. Nouvelle API Thread Builder

Dans Loom, nous avons la nouvelle API de générateur dans la classe Thread , ainsi que plusieurs méthodes d'usine. Voyons comment nous pouvons créer des usines standard et virtuelles et les utiliser pour l'exécution de nos threads:

Runnable printThread = () -> System.out.println(Thread.currentThread()); ThreadFactory virtualThreadFactory = Thread.builder().virtual().factory(); ThreadFactory kernelThreadFactory = Thread.builder().factory(); Thread virtualThread = virtualThreadFactory.newThread(printThread); Thread kernelThread = kernelThreadFactory.newThread(printThread); virtualThread.start(); kernelThread.start();

Voici la sortie de l'exécution ci-dessus:

Thread[Thread-0,5,main] VirtualThread[,ForkJoinPool-1-worker-3,CarrierThreads]

Ici, la première entrée est la sortie toString standard du thread du noyau.

Maintenant, nous voyons dans la sortie que le thread virtuel n'a pas de nom et qu'il s'exécute sur un thread de travail du pool Fork-Join du groupe de thread CarrierThreads .

Comme nous pouvons le voir, quelle que soit l'implémentation sous-jacente, l'API est la même, ce qui implique que nous pourrions facilement exécuter du code existant sur les threads virtuels .

De plus, nous n'avons pas besoin d'apprendre une nouvelle API pour les utiliser.

4. Composition du fil virtuel

C'est une suite et un ordonnanceur qui, ensemble, forment un fil virtuel. Maintenant, notre planificateur en mode utilisateur peut être n'importe quelle implémentation de l' interface Executor . L'exemple ci-dessus nous a montré que, par défaut, nous fonctionnons sur ForkJoinPool .

Maintenant, de la même manière qu'un thread du noyau - qui peut être exécuté sur le processeur, puis parqué, replanifié, puis reprend son exécution - une continuation est une unité d'exécution qui peut être démarrée, puis garée (cédée), replanifiée en arrière et reprend son exécution de la même manière là où elle s'est arrêtée et toujours gérée par une JVM au lieu de s'appuyer sur un système d'exploitation.

Notez que la suite est une API de bas niveau et que les programmeurs doivent utiliser des API de plus haut niveau comme l'API de générateur pour exécuter des threads virtuels.

Cependant, pour montrer comment cela fonctionne sous le capot, nous allons maintenant exécuter notre suite expérimentale:

var scope = new ContinuationScope("C1"); var c = new Continuation(scope, () -> { System.out.println("Start C1"); Continuation.yield(scope); System.out.println("End C1"); }); while (!c.isDone()) { System.out.println("Start run()"); c.run(); System.out.println("End run()"); }

Voici la sortie de l'exécution ci-dessus:

Start run() Start C1 End run() Start run() End C1 End run()

Dans cet exemple, nous avons exécuté notre continuation et, à un moment donné, avons décidé d'arrêter le traitement. Puis une fois que nous l'avons relancé, notre continuation a continué là où elle s'était arrêtée. Par la sortie, nous voyons que la méthode run () a été appelée deux fois, mais la continuation a été lancée une fois, puis a continué son exécution sur la deuxième exécution là où elle s'était arrêtée.

C'est ainsi que les opérations de blocage sont censées être traitées par la JVM. Une fois qu'une opération de blocage se produit, la continuation cède, laissant le thread porteur débloqué.

Donc, ce qui s'est passé, c'est que notre thread principal a créé un nouveau cadre de pile sur sa pile d'appels pour la méthode run () et a procédé à l'exécution. Ensuite, une fois la suite terminée, la JVM a enregistré l'état actuel de son exécution.

Ensuite, le fil conducteur a continué son exécution comme si le run () méthode retournée et est poursuivie avec le temps boucle. Après le deuxième appel à la méthode d' exécution de la continuation , la machine virtuelle Java a restauré l'état du thread principal au point où la continuation a abouti et a terminé l'exécution.

5. Conclusion

Dans cet article, nous avons discuté de la différence entre le thread du noyau et le thread virtuel. Ensuite, nous avons montré comment nous pourrions utiliser une nouvelle API de création de threads de Project Loom pour exécuter les threads virtuels.

Enfin, nous avons montré ce qu'est une continuation et comment cela fonctionne sous le capot. Nous pouvons explorer davantage l'état de Project Loom en inspectant la VM à accès anticipé. Alternativement, nous pouvons explorer davantage les API de concurrence Java déjà standardisées.