···11---
22created: "2024-02-05"
33+modified: "2024-02-15"
34date: "2024-02-05"
45language: fr
55-draft: true
66tags:
77 - Java
88-title: Tomcat 11 & Virtual Threads
88+ - Tomcat
99+title: Tomcat 11 & Virtual Threads 🧵
910---
10111112
12131313-_Apache Tomcat_ est le plus célèbre des conteneurs de _Servlets_ Java.
1414-Les versions se succèdent au fil des années. Avec _Spring Boot_, et son utilisation de la version "embedded", son usage en tant que serveur "installé" a diminué, mais il reste encore au coeur de la majorité de nos micro-services, parfois sans que les développeurs ne s'en rendent compte.
1414+Apache Tomcat est le plus célèbre des conteneurs de Servlets Java.
1515+Les versions se succèdent au fil des années. Avec Spring Boot, et son utilisation de la version «embedded», son usage en tant que serveur «installé» a diminué, mais il reste encore au cœur de la majorité de nos micro-services, parfois sans que les développeurs s'en rendent compte.
15161616-Chaque version majeure de _Tomcat_ apporte le support des nouvelles versions des API 'Java EE' ou 'JEE'.
1717+Chaque version majeure de Tomcat apporte le support des nouvelles versions des API `Java EE` ou `JEE`.
17181818-La version ayant eu le plus d'impact sur les développeurs est la version 10, qui a intégré le support des api `jakarta`, en remplacement des anciennes API `javax`, Cette version 10 de _Tomcat_ était liée à _Java 11_, dans laquelle suppression des packages `javax` liés à _Java EE_ a eu lieu. Les modules supprimés sont documentés dans la [JEP 320](https://openjdk.org/jeps/320). on y retrouve les tristement célèbres `java.xml.bind`, `javax.transcation` et `javax.activation`, qui ont donné du fil à retordre aux développeurs souhaitant migrer leurs applications.
1919+La version ayant eu le plus d'impact sur les développeurs est la version 10, qui a intégré le support des API `jakarta`, en remplacement des anciennes API `javax`. Cette version 10 de Tomcat était liée à Java 11, dans laquelle la suppression des packages `javax` liés à Java EE a eu lieu. Les modules supprimés sont documentés dans la [JEP 320](https://openjdk.org/jeps/320). On y retrouve les tristement célèbres `java.xml.bind`, `javax.transaction` et `javax.activation`, qui ont donné du fil à retordre aux développeurs souhaitant migrer leurs applications.
19202020-Les versions de _Tomcat_ sont donc à chaque fois compatibles avec une version minimale de Java, et des API `jakarta`.
2121+Les versions de Tomcat sont donc à chaque fois compatibles avec une version minimale de Java, et des API `jakarta`.
2122Le tableau ci-dessous reprend la liste des versions compatibles :
22232324| **Servlet Spec** | **Apache Tomcat Version** | **Supported Java Versions** | **Release date** |
···2829| 3.1 | 8.5.x | 7 and later | jan. 2014 |
2930| 3.0 | 7.0.x (archived) | 6 and later | jan. 2011 |
30313131-La version 11 de _Tomcat_ est donc dédiée à la version 21 de Java.
3232-Cette stratégie n'est pas surprenante en soit, la version 21 étant la dernière version LTS en date.
3232+La version 11 de Tomcat est donc destinée à la version 21 de Java.
3333+Cette stratégie n'est pas surprenante en soi, la version 21 étant la dernière version LTS à date.
33343434-Même si _Tomcat 11_ n'est pas encore en version finale, les travaux pour son développement durent depuis déjà plus d'un an à l'écriture de ces lignes. La première version _milestone_ de _Tomcat 11_ a été publiée en décembre 2022 ! La première version était prévue pour supporter Java 11 (cf la [release note Tomcat 11.0.0-M1](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M1/RELEASE-NOTES)). La version Java 17 a ensuite été choisie à partir de la _milestone_ 3 de _Tomcat 11_ (cf la [release note Tomcat 11.0.0-M3](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M3/RELEASE-NOTES)).
3535-La version 21 a été choisie à partir de la _milestone_ 7 de _Tomcat 11_, publiée en Juin 2023, soit 3 mois avant la sortie de Java 21 (cf la [release note Tomcat 11.0.0-M7](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M7/RELEASE-NOTES)).
3535+Même si Tomcat 11 n'est pas encore en version finale, les travaux pour son développement durent depuis déjà plus d'un an à l'écriture de ces lignes. La première version _milestone_ de Tomcat 11 a été publiée en décembre 2022 !
3636+La première version était prévue pour supporter Java 11 (cf. la [_release note_ Tomcat 11.0.0-M1](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M1/RELEASE-NOTES)).
3737+La version Java 17 a ensuite été choisie à partir de la _milestone_ 3 de Tomcat 11 (cf. la [_release note_ Tomcat 11.0.0-M3](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M3/RELEASE-NOTES)).
3838+La version 21 a été choisie à partir de la _milestone_ 7 de Tomcat 11, publiée en juin 2023, soit 3 mois avant la sortie de Java 21 (cf. la [_release note_ Tomcat 11.0.0-M7](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M7/RELEASE-NOTES)).
36393737-La version actuelle est la _milestone_ 16, publiée le 9 janvier 2024. C'est cette version qui sera testée dans cet article.
4040+La version actuelle est la _milestone_ 16, publiée le 9 janvier 2024. C'est cette version qui sera testée dans cet article.
38413939-Un des principaux avantages de cette version 11, liée à Java 21 est le support des _Virtual Threads_. Bien que le code nécessaire a été ajouté à Tomcat en version 10.1, on peut considérer que le support n'était qu'expérimental, puisque les _Virtual Threads_ n'ont été intégrés qu'en version _preview_ à partir de Java 19, et en version finale en Java 21.
4242+Un des principaux avantages de cette version 11, avec le support de Java 21, est le support des _Virtual Threads_. Bien que le code nécessaire ait été ajouté à Tomcat en version 10.1, on peut considérer que le support n'était qu'expérimental, puisque les _Virtual Threads_ n'ont été intégrés qu'en version _preview_ à partir de Java 19, et en version finale en Java 21.
40434144## C'est quoi les _Virtual Threads_ ?
42454343-Avant d'explorer l'implémentation de _Tomcat_ et son usage des _Virtual Threads_, un rapide rappel de ce qu'ils sont et de comment ils fonctionnent.
4646+Avant d'explorer l'implémentation de Tomcat et son usage des _Virtual Threads_, un rapide rappel de ce qu'ils sont et de la manière dont ils fonctionnent.
44474545-Les _Virtual Threads_ sont des _Thread_ dits "légers", parfois appelés "Green Threads" ou "Routines/Coroutines" dans d'autres langages. Ils sont mis en opposition aux _Threads_ dits "Plateforme". Les _Threads_ plateforme sont des _Threads_ gérés directement par le système d'exploitation.
4848+Les _Virtual Threads_ sont des _Thread_ dits «légers», parfois appelés «Green Threads» ou «Routines/Coroutines» dans d'autres langages. Ils sont mis en opposition aux _Threads_ dits «Plateforme». Les _Threads_ plateforme sont des _Threads_ gérés directement par le système d'exploitation.
46494747-> Une excellente conférence de José Paumard sur le projet Loom, qui introduit les _Threads Virtuels_ en Java est visible sur [Youtube](https://www.youtube.com/watch?v=v7DzKOniNh0). Cette vidéo est une très bonne introduction à ce sujet.
5050+> Une excellente conférence de José Paumard sur le projet Loom, qui introduit les _Virtual Threads_ en Java, est visible sur [Youtube](https://www.youtube.com/watch?v=v7DzKOniNh0). Cette vidéo est une très bonne introduction à ce sujet.
48514949-### Les Threads _Plateforme_
5252+### Les _Threads_ Plateforme
50535151-Lorsqu'un programme demande la création d'un _Thread_, le système d'exploitation stoppe l'exécution du code et crée le _Thread_, avec sa mémoire dédiée, appellée la _Stack_. Il redonne ensuite la main au programme pourqu'il continue son exécution.
5454+Lorsqu'un programme demande la création d'un _Thread_, le système d'exploitation stoppe l'exécution du code et crée le _Thread_, avec sa mémoire attribuée, appelée la _Stack_. Il redonne ensuite la main au programme pour qu'il continue son exécution.
5255Ces deux étapes impliquent, à chaque fois, que le CPU sauvegarde l'état courant de l'exécution du programme, et le restaure ensuite. C'est ce qu'on appelle un _context switch_, un changement de contexte d'exécution.
53565454-Lors de la création d'un _Thread_, le système d'exploitation doit donc effectuer plusieurs _context switchs_, et allouer un peu de mémoire au _Thread_. Ces étapes ont donc un coût, en temps et en mémoire.
5757+Lors de la création d'un _Thread_, le système d'exploitation doit donc effectuer plusieurs _context switches_, et allouer un peu de mémoire au _Thread_. Ces étapes ont donc un coût, en temps et en mémoire.
55585659#### Le coût en temps
57605861Le temps de création d'un _Thread_ dépend principalement du système d'exploitation et de sa charge actuelle.
5959-Pour mesurer ce temps, un benchmark écrit avec l'outil [JMH](https://github.com/openjdk/jmh) (dont l'utilisation vaudrait un article à elle seule) permet d'estimer le temps de démarrage d'un _Thread_ Java sur une machine :
6262+Pour mesurer ce temps, un _benchmark_ écrit avec l'outil [JMH](https://github.com/openjdk/jmh) (dont l'utilisation vaudrait un article à elle seule) permet d'estimer le temps de démarrage d'un _Thread_ Java sur une machine :
60636164```java
6265@BenchmarkMode(Mode.AverageTime)
···71747275 @Benchmark
7376 public void computeInPlatformThread() throws InterruptedException {
7474- // exécution dans un thread plateforme dédié
7777+ // exécution dans un thread plateforme
7578 var thread = Thread.ofPlatform().start(() -> {
7679 Blackhole.consumeCPU(1024);
7780 });
···8184 public static void main(String[] args) throws RunnerException {
8285 Options opt = new OptionsBuilder()
8386 .include(ThreadsBenchmark.class.getSimpleName())
8484- .warmupIterations(1) // une itération de pré-chauffage de la JVM
8787+ .warmupIterations(1) // une itération de préchauffage de la JVM
8588 .measurementIterations(3) // 3 itérations de mesure
8689 .forks(1)
8790 .build();
···9295}
9396```
94979595-Les deux méthodes annotées `@Benchmark` sont exécutées en boucle pendant 10 secondes pour mesurer le temps moyen de leur exécution, et cela 4 fois en tout, une première fois pour pré-chauffer la JVM (warmup), et 3 fois pour mesurer les performances réelles. Le `fork(1)` permet de créer une JVM dédiée à l'exécution des tests.
9898+Les deux méthodes annotées `@Benchmark` sont exécutées en boucle pendant 10 secondes pour mesurer le temps moyen de leur exécution, et cela 4 fois en tout : une première fois pour préchauffer la JVM (_warm-up_), et 3 fois pour mesurer les performances réelles. La ligne `forks(1)` permet de préciser de créer une JVM destinée à l'exécution des tests.
96999797-La première méthode effectue un calcul au combien inutile, à travers la classe `Blackhole` fournie par JMH. La seconde méthode effectue ce même calcul, mais dans un _Thread_ plateforme dédié et attend la fin son exécution. De cette manière, on peut extrapoler le surcoût de l'exécution de la tâche dans un _Thread_, surcoût qui comprend donc la création du _Thread_, et sa suppression.
100100+La première méthode effectue un calcul ô combien inutile, à travers la classe `Blackhole` fournie par JMH. La seconde méthode effectue ce même calcul, mais dans un _Thread_ plateforme et attend la fin de son exécution. De cette manière, on peut extrapoler le surcoût de l'exécution de la tâche dans un _Thread_, surcoût qui comprend donc la création du _Thread_, et sa suppression.
981019999-Le résultat de l'exécution du benchmark est le suivant :
102102+Le résultat de l'exécution du _benchmark_ est le suivant :
100103101104```bash
102105Benchmark Mode Cnt Score Error Units
···104107ThreadsBenchmark.computeInPlatformThread avgt 3 0.038 ± 0.015 ms/op
105108```
106109107107-On observe que le `Blackhole.consumeCPU(1024)` s'exécute en moyenne en 0.002 millisecondes. L'exécution de la même instruction dans un _Thread_ dédié se fait en 0.038 millisecondes. Le surcoût lié à la création et destruction du _Thread_ est donc de 0.036 millisecondes.
110110+On observe que le `Blackhole.consumeCPU(1024)` du premier _benchmark_ s'exécute en moyenne en 0,002 millisecondes. L'exécution de la même instruction dans un _Thread_ plateforme se fait en 0,038 millisecondes. Le surcoût lié à la création et destruction du _Thread_ est donc de 0,036 millisecondes.
108111109109-> Créer un Thread pour effectuer un calcul peut donc être contre-productif !
112112+> Créer un _Thread_ pour effectuer un calcul peut donc être contre-productif ! 😱
110113111114#### Le coût en mémoire
112115113113-Le coût en mémoire d'un _Thread_ est connu à l'avance et contrôlé par les paramètres `-Xss` ou `-XX:ThreadStackSize` de la JVM. Cependant, attention aux confusions. On parle bien ici de mémoire réservée, et non pas de mémoire effectivement utilisée. Pour un Thread qui ne remplit pas sa _stack_, sa consommation réelle sera bien moindre.
116116+Le coût en mémoire d'un _Thread_ est connu à l'avance et contrôlé par les paramètres `-Xss` ou `-XX:ThreadStackSize` de la JVM. Cependant, attention aux confusions. On parle bien ici de mémoire réservée, et non pas de mémoire effectivement utilisée. Pour un_Thread_qui ne remplit pas sa _Stack_, sa consommation réelle sera bien moindre.
114117115118La commande suivante permet de constater les valeurs par défaut de la mémoire d'un _Thread_ Java :
116119···122125intx VMThreadStackSize = 1024 {pd product} {default}
123126```
124127125125-La valeur est exprimé en kilo-octets. Un _Thread_ réservera donc 1024 ko de RAM, soit 1 Mo. 200 _Threads_ réserveront donc 200 Mo de RAM native, en plus de la RAM allouée à la _heap_ Java.
128128+La valeur est exprimée en kilo-octets. Un _Thread_ réservera donc 1 024 ko de RAM, soit 1 Mo. 200 _Threads_ réserveront donc 200 Mo de RAM native, en plus de la RAM allouée à la _heap_ Java.
126129127127-### Les Threads _Virtuels_
130130+### Les _Virtual Threads_
128131129129-Les _Threads Virtuels_ sont créés, orchestrés et exécutés directement par la JVM, qui se charge de gérer leur stack et leur exécution de manière interne. La création d'un _Thread Virtuel_ n'implique donc pas forcément la création d'un _Thread_ plateforme.
130130-Le coût de création d'un _Thread Virtuel_ est donc bien inférieur à un _Thread_ plateforme, puisqu'il ne nécessite pas de _context switch_, ni d'allocation d'un bloc de mémoire dédié.
132132+Les _Virtual Threads_ sont créés, orchestrés et exécutés directement par la JVM, qui se charge de gérer leur _Stack_ et leur exécution de manière interne. La création d'un _Virtual Thread_ n'implique donc pas forcément la création d'un _Thread_ plateforme.
133133+Le coût de création d'un _Virtual Thread_ est donc bien inférieur au coût d'un _Thread_ plateforme, puisqu'il ne nécessite pas de _context switch_, ni d'allocation d'un bloc de mémoire.
131134132132-On peut mesurer le coût temporel de la création d'un _Thread Virtuel_ en ajoutant cette méthode à notre benchmark précédent :
135135+On peut mesurer le coût temporel de la création d'un _Virtual Thread_ en ajoutant cette méthode à notre _benchmark_ précédent :
133136134137```java
135138@Benchmark
···141144}
142145```
143146144144-Notez l'usage de `Thread.ofVirtual()` pour créer un _Thread Virtuel_ en lieu et place du `Thread.ofPlatform()`.
147147+Notez l'usage de `Thread.ofVirtual()` pour créer un _Virtual Thread_ en lieu et place du `Thread.ofPlatform()`.
145148146149Les durées d'exécution observées sont les suivantes :
147150···152155ThreadsBenchmark.computeInVirtualThread avgt 3 0.005 ± 0.002 ms/op
153156```
154157155155-Le benchmark utilisant les _Threads Virtuels_ présente un surcoût d'exécution de 0.002 millisecondes par rapport à l'exécution dans le _Thread_ principal, mais est largement inférieur au surcoût lié à l'exécution dans un _Thread Plateforme_.
158158+Le _benchmark_ utilisant les _Virtual Threads_ présente un surcoût d'exécution de 0,003 millisecondes par rapport à l'exécution dans le _Thread_ principal, mais est largement inférieur au surcoût lié à l'exécution dans un _Thread_ plateforme.
156159157157-> Le coût d'exécution en temps d'un _Thread Virtuel_ est donc 15 fois inférieur à un Thread plateforme.
160160+> Le coût de création en temps d'un _Virtual Thread_ est donc 15 fois inférieur à un _Thread_ plateforme.
158161159159-Notes qu'avant l'avènement des _Threads Virtuels_, le problème du coût de création des _Threads Plateforme_ était souvent adressé par l'utilisation de pools de _Threads_, qui permettent de ré-utiliser des _Threads_ existants (vive le recyclage ♻️),plutôt que de les re-créer.
162162+Notez qu'avant l'avènement des _Virtual Threads_, le problème du coût de création des _Threads_ plateforme était souvent adressé par l'utilisation de _pools_ de _Threads_, qui permettent de réutiliser des _Threads_ existants (vive le recyclage ♻️), plutôt que de les recréer.
160163161164## L'implémentation de Tomcat
162165163166
164167165165-Dans le code de _Tomcat_, l'interface `Executor` décrit les objets qui ont pour responsabilité d'exécuter les requêtes entrantes. Depuis la version 10.1 de _Tomcat_, cette interface a deux implémentations. L'implémentation historique `StandardThreadExecutor`, qui s'appuie sur un pool de _Threads_ _workers_ et une `BlockingQueue` de taille fixe pour les requêtes entrantes, et la nouvelle implémentation `StandardVirtualThreadExecutor` qui utilise un _Thread Virtuel_ pour exécuter chaque requête entrante.
168168+Dans le code de Tomcat, l'interface `Executor` décrit les objets qui ont pour responsabilité d'exécuter les requêtes entrantes. Depuis la version 10.1 de Tomcat, cette interface a deux implémentations. L'implémentation historique `StandardThreadExecutor`, qui s'appuie sur un _pool_ de _Threads_ _workers_ et une `BlockingQueue` de taille fixe pour les requêtes entrantes, et la nouvelle implémentation `StandardVirtualThreadExecutor` qui utilise un _Virtual Thread_ pour exécuter chaque requête entrante.
166169167167-En farfouillant dans le code de Tomcat, on peut observer cette implémentation dans la classe `VirtualThreadExecutor`, qui est utilisée par le `StandardVirtualThreadExecutor` :
170170+En fouillant dans le code de Tomcat, on peut observer cette implémentation dans la classe `VirtualThreadExecutor`, qui est utilisée par le `StandardVirtualThreadExecutor` :
168171169172```java
170173public class VirtualThreadExecutor extends AbstractExecutorService {
···185188}
186189```
187190188188-> Il est par ailleurs surprenant que Tomcat aie choisi de développer son propre `ExecutorService`, au lieu d'utiliser celui construit par `Executors.newVirtualThreadPerTaskExecutor()`. Il semble que ce choix soit lié à la gestion de l'arrêt de l'`ExecutorService` qui est implémentée du côté du `ThreadPoolExecutor`.
191191+> Il est par ailleurs surprenant que Tomcat ait choisi de développer son propre `ExecutorService`, au lieu d'utiliser celui construit par `Executors.newVirtualThreadPerTaskExecutor()`. Il semble que ce choix soit lié à la gestion de l'arrêt de l'`ExecutorService` qui est implémentée du côté du `ThreadPoolExecutor`.
189192190190-## Le benchmark
193193+## Le _benchmark_
191194192195Dans cette section, nous allons tester les performances de deux versions de Tomcat :
193196194194-- la version 10.1, sans support des _Threads Virtuels_
195195-- la version 11.0.0-M16, avec support des _Threads Virtuels_ activés
197197+- la version 10.1, sans support des _Virtual Threads_ ;
198198+- la version 11.0.0-M16, avec support des _Virtual Threads_ activés.
196199197197-Pour monter l'environnement de test, j'ai installé une version 21 de Java, en particulier le build _eclipse-temurin_ disponible chez [adoptium.net](https://adoptium.net/temurin/releases/?version=21) :
200200+Pour monter l'environnement de test, j'ai installé une version 21 de Java, en particulier le _build_ _eclipse-temurin_ disponible chez [adoptium.net](https://adoptium.net/temurin/releases/?version=21) :
198201199202```bash
200203java --version
···203206OpenJDK 64-Bit Server VM Temurin-21.0.1+12 (build 21.0.1+12-LTS, mixed mode, sharing)
204207```
205208206206-J'ai aussi installé les version 10 et 11 de Tomcat :
209209+J'ai aussi installé les versions 10 et 11 de Tomcat :
207210208208-- la dernière version disponible de [Tomcat 10](https://tomcat.apache.org/download-10.cgi), la 10.1.18
209209-- la dernière version disponible de [Tomcat 11](https://tomcat.apache.org/download-11.cgi), la 11.0.0-M16
211211+- la dernière version disponible de [Tomcat 10](https://tomcat.apache.org/download-10.cgi), la 10.1.18 ;
212212+- la dernière version disponible de [Tomcat 11](https://tomcat.apache.org/download-11.cgi), la 11.0.0-M16.
210213211211-Ma machine de test est équipée d'un CPU _11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz_ et de 64Go de RAM (!).
214214+Ma machine de test est équipée d'un CPU _11<sup>th</sup> Gen Intel(R) Core(TM) i7-1165G7 @ 2.80 GHz_ et de 64 Go de RAM (!).
212215213213-Les JVM sont démarrées avec l'otion suivantes `-Xms512m -Xmx512m` pour positionner une taille de la heap à 512 Mo directement consommée.
214214-L'option `-XX:NativeMemoryTracking=summary` permet d'observer la consommation mémoire de la JVM.
216216+Les JVM sont démarrées avec les options `-Xms512m -Xmx512m` pour positionner une taille de la _heap_ à 512 Mo directement consommée.
217217+L'option `-XX:NativeMemoryTracking=summary` permet d'observer la consommation mémoire de la JVM, pour analyser plus finement les tailles de mémoire réservées et consommées auprès du système d'exploitation.
215218216219```bash
217220export CATALINA_OPTS='-Xms512m -Xmx512m -XX:NativeMemoryTracking=summary'
···219222220223> Je n'ai pas positionné de paramétrage propre au GC ou d'autres options, ce qui m'intéresse ce sont uniquement la consommation de RAM liée aux _Threads_ et les performances liées à des temps de réponse aux requêtes.
221224222222-### La configuration de Tomcat 11
225225+### La configuration de Tomcat 11
223226224224-_Pour_ utiliser les _Threads Virtuels_ dans Tomcat 11, il faut paramétrer l'_Executor_ de Tomcat pour utiliser la classe qui instancie de _Threads Virtuels_, en lieu et place de l'implémentation standard qui utilise un pool de _Threads Plateforme_, et assigner l'exécutor au _Connector_. Ce paramétrage n'est pas actif par défaut. Il se fait dans le fichier `settings.xml`, dans la balise `<Service>`, comme indiqué dans [la documentation](https://tomcat.apache.org/tomcat-11.0-doc/config/executor.html#Virtual_Thread_Implementation) :
227227+Pour utiliser les _Virtual Threads_ dans Tomcat 11, il faut paramétrer l'_Executor_ de Tomcat pour activer la classe qui instancie les _Virtual Threads_, en lieu et place de l'implémentation standard qui utilise un _pool_ de _Threads_ plateforme, et assigner l'_Executor_ au _Connector_ en charge d'écouter sur le port HTTP. Ce paramétrage n'est pas actif par défaut. Il se fait dans le fichier `settings.xml`, dans la balise `<Service>`, comme indiqué dans [la documentation](https://tomcat.apache.org/tomcat-11.0-doc/config/executor.html#Virtual_Thread_Implementation) :
225228226229```xml
227230<Service name="Catalina">
···241244</Service>
242245```
243246244244-On paramètre donc le `StandardVirtualThreadExecutor` comme devant traiter les requêtes allouées au _Connector_ écoutant sur le port `8080`.
247247+On paramètre donc le `StandardVirtualThreadExecutor` comme devant traiter les requêtes allouées au _Connector_ écoutant sur le port `8080`.
245248246246-Aucune autre configuration n'est nécessaire. Aucune configuration particulière n'est fait sur le Tomcat 10.1.
249249+Aucune autre configuration n'est nécessaire sur le Tomcat 11. Aucune configuration particulière n'est faite sur le Tomcat 10.1 pour les tests.
247250248248-### Les perfs attendues
251251+### Les performances attendues
249252250250-On s'attend, entre Tomcat 10.1 et Tomcat 11, avec l'utilisation des _Threads Virtuels_, d'avoir une empreinte mémoire réservée inférieure, ainsi que de meilleures performances à l'exécution des requêtes.
251251-En principe, les _Threads Virtuels_ ne devraient utiliser que quelques _Threads Plateforme_, et donc limiter les _context switch_ en cas de charge importante.
253253+On s'attend, entre Tomcat 10.1 et Tomcat 11, avec l'utilisation des _Virtual Threads_, à avoir une empreinte mémoire réservée inférieure, ainsi que de meilleures performances à l'exécution des requêtes.
254254+En principe, les _Virtual Threads_ utilisés par Tomcat 11 ne devraient utiliser que quelques _Threads_ plateforme hôtes pour leur exécution, et donc limiter les _context switches_ en cas de charge importante.
252255253256### Démarrage et empreinte mémoire à vide
254257255255-#### Tomcat 10.1
258258+#### Tomcat 10.1
256259257257-Le Tomcat 10.1 est démarré avec la commande `startup.sh` :
260260+Tomcat 10.1 est démarré avec la commande `startup.sh` :
258261259262```bash
260263./startup.sh
···267270Tomcat started.
268271```
269272270270-L'empreinte mémoire de notre _Tomcat_ se fait avec la séquence de commandes :
273273+La récupération de l'empreinte mémoire de notre Tomcat se fait à l'aide des commandes `jps` et `jcmd` :
271274272275```bash
273276# listing des JVM en cours d'exécution
···297300298301```
299302300300-On observe que notre Heap est bien réservée à 512 Mo (524288KB), et que 41 _Threads_ ont été démarrés (dont les 25 _Threads_ liés à notre `Executor`), pour une consommation de 41 Mo supplémentaires. Nous avons un total de mémoire consommée de près de 630 Mo, car d'autres espaces sont réservés par la JVM (espaces de code, etc...).
303303+On observe que notre _Heap_ est bien réservée à 512 Mo (524 288 KB), et que 41 _Threads_ ont été démarrés (dont les 25 _Threads_ liés à notre `Executor`), pour une consommation de 41 Mo supplémentaires. Nous avons un total de mémoire consommée de près de 630 Mo, car d'autres espaces sont réservés par la JVM (espaces de code, etc.).
301304302302-En générant un peu de charge sur les applis exemples par défaut, on force _Tomcat_ à instancier les _Threads_ supplémentaires pour atteindre les 200 _Threads_.
305305+En générant un peu de charge sur les applications exemples par défaut, on force Tomcat à instancier les _Threads_ supplémentaires pour atteindre les 200 _Threads_.
306306+307307+La charge est générée avec la commande [`hey`](https://github.com/rakyll/hey), en utilisant 400 workers pour envoyer un million de requêtes à la _Servlet_ d'exemple.
303308304309```bash
305310$ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample
311311+```
306312313313+On récupère ensuite l'empreinte mémoire de notre Tomcat pour observer les nouvelles valeurs :
314314+315315+```bash
307316$ jcmd $(jps -l | grep -v 'jps' | cut -d ' ' -f 1) VM.native_memory
308317309318Native Memory Tracking:
···324333 (arena=269KB #460) (peak=317KB #52)
325334```
326335327327-On observe que le nombre de _Threads_ est passé à 231, et qu'on a maintenant plus de 230 Mo réservés pour les _Threads_.
336336+On observe que le nombre de _Threads_ est passé à 231, et qu'on a maintenant plus de 230 Mo réservés pour les _Threads_.
328337329329-#### Tomcat 11
338338+#### Tomcat 11
330339331331-Comme pour le Tomcat 10.1, le Tomcat 11 est démarré :
340340+Comme pour Tomcat 10.1, Tomcat 11 est démarré :
332341333342```bash
334343$ ./bin/startup.sh
···364373 (arena=34KB #60) (peak=317KB #52)
365374```
366375367367-On observe qu'à froid, moins de _Threads_ sont alloués au démarrage, seulement 31 au lieu des 41 _Threads_ démarrés par Tomcat 10.1.
376376+On observe qu'à froid, moins de _Threads_ sont alloués au démarrage, seulement 31 au lieu des 41 _Threads_ démarrés par Tomcat 10.1.
368377369369-Après avoir passé une charge identique au test du Tomcat 10.1 :
378378+Après avoir passé une charge identique au test du Tomcat 10.1, toujours avec la commande`hey` :
370379371380```bash
372381$ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample
382382+```
373383384384+On récupère à nouveau l'empreinte mémoire de Tomcat 11 :
385385+386386+```bash
374387$ jcmd $(jps -l | grep -v 'jps' | cut -d ' ' -f 1) VM.native_memory
375388376389Native Memory Tracking:
···392405393406```
394407395395-On observe que Tomcat a instancié quelques _Threads_ en plus, pour passer à 39 et on atteint donc les 39 Mo de stack allouée.
396396-On économise donc pas loin de 200 Mo comme attendu.
408408+On observe que Tomcat a instancié quelques _Threads_ en plus, pour passer à 39 et on atteint donc les 39 Mo de _stack_ alloués.
409409+On économise donc pas loin de 200 Mo comme attendu.
397410398398-> Attention, cette mémoire est bien de la mémoire réservée, et non pas l'empreinte de la mémoire réelle consommée (dénommée `committed`). Les OS utilisent des mécanismes de mémoire virtuelle qui permettent de promettre de la mémoire à un process qui la demande. Cette mémoire n'est pas écrite sur la RAM tant qu'elle n'est pas réellement consommée.
411411+> Attention, cette mémoire est bien de la mémoire réservée, et non pas l'empreinte de la mémoire réelle consommée (dénommée _committed_). Les OS utilisent des mécanismes de mémoire virtuelle qui permettent de promettre de la mémoire à un _process_ qui la demande, même si la mémoire n'est pas disponible physiquement. Cette mémoire n'est pas écrite sur la RAM tant qu'elle n'est pas réellement consommée.
399412400400-Comme on pouvait s'y attendre, l'empreinte de la mémoire réservée par Tomcat pour les _Threads_ est plus faible. Cependant, comme cette mémoire n'est pas utilisée, l'impact sur les performances est faible. L'intérêt des _Threads Virtuels_ ne réside pas principalement dans cette éventuelle économie.
413413+Comme on pouvait s'y attendre, l'empreinte de la mémoire réservée par Tomcat pour les _Threads_ est plus faible. Cependant, comme cette mémoire n'est pas utilisée, l'impact sur les performances est faible. L'intérêt des _Virtual Threads_ ne réside pas principalement dans cette éventuelle économie.
401414402415### Performances avec une Servlet simple
403416404404-Pour mesurer les performances de Tomcat 10 et 11, j'utilise la commande `hey`, pour exécuter 1 million de requêtes, dans 400 workers différents.
417417+Pour mesurer les performances de Tomcat 10 et 11, j'utilise la commande `hey`, pour exécuter 1 million de requêtes, dans 400 _workers_ différents.
405418406419> Notez que je lance cette commande sur la même machine que ma machine de test, ce qui n'est clairement pas idéal, mais c'est suffisant pour ces tests.
407420408408-Je requête la servlet `HelloWorldExample`, qui est fournie avec Tomcat. Cette servlet affiche simplement une page web contenant le message _Hello World_.
421421+Je requête la Servlet `HelloWorldExample`, qui est fournie avec Tomcat. Cette Servlet affiche simplement une page web contenant le message _Hello World_.
409422410410-#### Tomcat 10.1 - Threads Plateforme
423423+#### Tomcat 10.1 - Threads Plateforme
411424412425```
413426hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample
···447460448461```
449462450450-Sur ce premier tir avec Tomcat 10.1, le temps moyen d'exécution est de 3.4 millisecondes, et 99% des requêtes ont reçu une réponse en moins de 9.7 millisecondes.
463463+Sur ce premier tir avec Tomcat 10.1, le temps moyen d'exécution est de 3,4 millisecondes, et 99 % des requêtes ont reçu une réponse en moins de 9,7 millisecondes.
451464452452-#### Tomcat 11 - Threads Virtuels
465465+#### Tomcat 11 - _Virtual Threads_
453466454454-Le même test a été lancé sur Tomcat 11 configuré avec des _Threads Virtuels_ :
467467+Le même test a été lancé sur Tomcat 11 configuré avec des _Virtual Threads_ :
455468456469```
457470hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample
···490503 99% in 0.0080 secs
491504```
492505493493-Le temps moyen d'exécution est de 3,1 millisecondes, et 99% des réponses ont été données en moins de 9 millisecondes.
494494-On a une amélioration de performances de près de 10% pour une simple servlet !
506506+Le temps moyen d'exécution est de 3,1 millisecondes, et 99 % des réponses ont été données en moins de 9 millisecondes.
507507+On a une amélioration des performances de près de 10 % pour une simple Servlet !
495508496496-On peut facilement interpréter cette amélioration. Les performances accrues sont probablement liées au fait que le système d'exploitation ne doit pas switcher entre l'exécution de 200 Threads en paralèlle dans le cas de Tomcat 11. Ce qui occasionne donc plus de temps disponible, et donc des meilleurs temps de réponse.
509509+On peut facilement interpréter cette amélioration. Les performances accrues sont probablement liées au fait que le système d'exploitation ne doit pas _switcher_ entre l'exécution de 200 _Threads_ en paralèlle dans le cas de Tomcat 11, ce qui occasionne donc plus de temps disponible, et donc des meilleurs temps de réponse.
497510498511### Performances avec une Servlet effectuant un appel bloquant
499512500500-Pour aller un peu plus loin, nous allons exécuter un tir de performances similaire, avec une Servlet effectuant un appel bloquant de 50 millisecondes : `Thread.sleep(50)` :
513513+Pour aller un peu plus loin, nous allons exécuter un tir de performances similaire, avec une Servlet effectuant un appel bloquant de 50 millisecondes avec `Thread.sleep(50)` :
501514502515```java
503516public class ThreadInfo extends HttpServlet {
504517505518 @Override
506519 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
507507- response.setContentType("text/html");
508508- response.setCharacterEncoding("UTF-8");
509509-510510- PrintWriter out = response.getWriter();
511511- out.println("<!DOCTYPE html><html>");
512512- out.println("<head>");
513513- out.println("<meta charset=\"UTF-8\" />");
514514-515515- out.println("<title>Thread info</title>");
516516- out.println("</head>");
517517- out.println("<body><h1>" + Thread.currentThread().getName() + "</h1></body>");
518518-519520 try {
520521 Thread.sleep(50L); // fais dodo
521522 } catch (InterruptedException ex) {
···531532```
532533533534Quel est l'impact attendu ?
534534-Pour Tomcat 10.1, qui dispose de 200 _Threads_ maximum, on s'attend à obtenir un débit de 4000 requêtes par secondes maximum (200 threads \* 1000 ms / 50 ms ), donc un temps d'exécution total de 250 secondes (1 million de requêtes / 4000 req / seconde ).
535535-Pour Tomcat 11, non limité par des _Threads_, on s'attend à obtenir un débit similaire au test précédent.
535535+Pour Tomcat 10.1, qui dispose de 200 _Threads_ maximum, on s'attend à obtenir un débit de 4 000 requêtes par seconde maximum (200 _Threads_ \* 1 000 ms / 50 ms), donc un temps d'exécution total de 250 secondes (1 million de requêtes / 4 000 req / s).
536536+537537+Pour Tomcat 11, non limité par des _Threads_, on s'attend à obtenir un débit similaire au test de la Servlet précédente sans les appels bloquants.
536538537537-#### Tomcat 10.1 - Threads Plateforme - Appels Bloquants
539539+#### Tomcat 10.1 - _Threads_ plateforme - appels bloquants
538540539539-Le tir de performances sur le Tomcat 10.1 donne le résultat suivant :
541541+Le tir de performances sur Tomcat 10.1 donne le résultat suivant :
540542541543```
542544$ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/ThreadInfo
···575577 99% in 0.1451 secs
576578```
577579578578-Les 250 secondes attendues sont bien réelles et on observe un débit à 3967 requêtes par secondes. 99% des requêtes ont une réponse en moins de 145 millisecondes. Cette performance n'est pas terrible, quand on met en lumière le fait que l'opération bloquante n'est que de 50 millisecondes. La requête la plus rapide a bien été exécutée en 50 millisecondes, mais en moyenne, l'exécution est de 100 millisecondes.
580580+Les 250 secondes attendues pour le temps d'exécution sont bien réelles et on observe un débit à 3 967 requêtes par seconde. 99 % des requêtes ont une réponse en moins de 145 millisecondes. Cette performance n'est pas terrible, quand on met en lumière le fait que l'opération bloquante n'est que de 50 millisecondes. La requête la plus rapide a bien été exécutée en 50 millisecondes, mais en moyenne, l'exécution est de 100 millisecondes.
579581580580-Cette lenteur supplémentaire est probablement liée au _context switch_ que doit exécuter le système d'exploitation. Le débit observé de moins de 4000 requêtes par seconde est bien lié à la contrainte des 200 s;threads bloqués et occupés pendant 50 millisecondes chacun.
582582+Cette lenteur supplémentaire est liée au temps d'attente des requêtes pour obtenir un _Thread_ disponible. Passé le premier lot de 200 requêtes, les autres attendent 50 millisecondes avant d'obtenir un _Thread_, qui lui même bloque pendant 50 millisecondes le traitement d'autres requêtes. Le débit observé de moins de 4 000 requêtes par seconde est bien lié à la contrainte des 200 _Threads_ bloqués et occupés pendant 50 millisecondes chacun.
581583582582-#### Tomcat 11
584584+#### Tomcat 11 - _Virtual Threads_ - appels bloquants
583585584584-Le même tir de performances sur Tomcat 11 configuré avec les _Threads Virtuels_ donne un résultat complètement différent :
586586+Le même tir de performances sur Tomcat 11 configuré avec les _Virtual Threads_ donne un résultat complètement différent :
585587586588```
587589$ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/ThreadInfo
···620622 99% in 0.0544 secs
621623```
622624623623-On observe que le temps moyen de réponse à une requête est bien de 50 millisecondes. Aucune surcharge liée à du _context switch_ n'est observée ici. 99% des requêtes sont répondues en 54 millisecondes.
625625+On observe que le temps moyen de réponse à une requête est bien de 50 millisecondes. Aucune surcharge liée à du _context switch_ n'est observée ici. 99 % des requêtes obtiennent une réponse en 54 millisecondes.
624626625625-Attention cependant, on observe que le débit est de seulement 7900 requêtes par seconde. La limitation ici est liée au nombre de workers de ma commande `hey` positionné à 400.
627627+Attention cependant, on observe que le débit est de seulement 7 900 requêtes par seconde. La limitation ici est liée au nombre de _workers_ positionné à 400 sur la ma commande `hey`. La commande n'envoie pas suffisamment de requêtes pour atteindre le débit théorique maximum.
626628627627-Un test rapide permet d'augmenter le nombre de workers à 1000 pour observer la différence :
629629+Un second test avec le nombre de _workers_ à 1 000 permet d'observer la différence de débit :
628630629631```
630632$ hey -c 1000 -n 1000000 http://localhost:8080/examples/servlets/servlet/ThreadInfo
···663665 99% in 0.0568 secs
664666```
665667666666-Avec 1000 workers, le temps moyen de réponse reste autour de 50 millisecondes. 99% des requêtes reçoivent une réponse en moins de 58 millisecondes. Le débit passe à 1900 requêtes par seconde !
668668+Avec 1 000 _workers_, le temps moyen de réponse reste autour de 50 millisecondes. 99 % des requêtes reçoivent une réponse en moins de 58 millisecondes. Le débit passe à 19 000 requêtes par seconde !
667669668670On atteint malheureusement ici les limites de ma machine, puisque à ce stade quelques erreurs sont observées : `dial tcp 127.0.0.1:8080: socket: too many open files`.
669671670672Cependant, ces performances laissent deviner qu'il serait possible d'aller encore plus loin.
671673672672-## Bonus, avec Spring Boot 3
674674+## Bonus, avec Spring Boot 3
673675674674-> "Julien, tu es bien gentil avec tes Servlets, mais plus personne n'en développe."
676676+> 🤓 « Julien, tu es bien gentil avec tes Servlets, mais plus personne n'en développe. »
675677676676-Cette partie "bonus" teste le même comportement, mais avec Spring Boot 3 !
678678+Cette partie «bonus» teste le même comportement, mais avec Spring Boot 3 !
677679678678-### Configurer Spring Boot 3
680680+### Configurer Spring Boot 3
679681680680-Malheureusement, il n'est pas possible pour le moment d'utiliser Tomcat 11 avec Spring Boot 3.
681681-Néanmoins, Spring Boot 3 a intégré le support des _Threads Virtuels_ et de l'Exécutor Tomcat à Tomcat 10 !
682682+Malheureusement, il n'est pas possible pour le moment d'utiliser Tomcat 11 avec Spring Boot 3.
683683+Néanmoins, Spring Boot 3 a intégré le support des _Virtual Threads_ et de l'_Executor_ _VirtualThreadExecutor_ à Tomcat 10 !
682684683683-Pour utiliser les Virtual Threads dans Spring Boot 3, il faut positionner la properties suivante :
685685+Pour utiliser les _Virtual Threads_ dans Spring Boot 3, il faut positionner la _properties_ suivante :
684686685687```properties
686688spring.threads.virtual.enabled=true
687689```
688690689689-Côté Spring Boot, cette properties est interprétée par l'annotation `@ConditionalOnThreading` et configure un `TomcatVirtualThreadsWebServerFactoryCustomizer` :
691691+Aucune autre modification n'est nécessaire !
692692+693693+Pour comprendre comment cette _properties_ opère sa magie, il faut parcourir le code de Spring Boot.
694694+Cette _properties_ est interprétée par l'annotation `@ConditionalOnThreading` et configure un `TomcatVirtualThreadsWebServerFactoryCustomizer` :
690695691696```java
692697@Configuration(proxyBeanMethods = false)
···702707}
703708```
704709705705-Le `TomcatVirtualThreadsWebServerFactoryCustomizer` configure le Tomcat embedded pour utiliser l'executor `VirtualThreadExecutor` :
710710+Le `TomcatVirtualThreadsWebServerFactoryCustomizer` configure le Tomcat _embedded_ pour utiliser l'_Executor_ `VirtualThreadExecutor` :
706711707712```java
708713public class TomcatVirtualThreadsWebServerFactoryCustomizer
···716721}
717722```
718723719719-Dans notre code, un simple `@Controller` Spring permet de re-créer le comportement équivalent à la servlet utilisée pour le benchmark précédent :
724724+Dans notre code, un simple `@Controller` Spring permet de recréer le comportement équivalent à la Servlet utilisée pour le _benchmark_ précédent :
720725721726```java
722727@RestController
···730735}
731736```
732737733733-Avec la properties `spring.threads.virtual.enabled=false`, on obtient les performances suivantes, similaires à ce qu'on avait en utilisant Tomcat 10.1, sans support des Threads Virtuels :
738738+Avec la _properties_ `spring.threads.virtual.enabled=false`, on obtient les performances suivantes, similaires à ce qu'on avait en utilisant Tomcat 10.1, sans support des _Virtual Threads_ :
734739735740```
736736-hey -c 400 -n 1000000 http://localhost:8080
741741+$ hey -c 400 -n 1000000 http://localhost:8080
737742738743Summary:
739744 Total: 253.9172 secs
···769774 99% in 0.1352 secs
770775```
771776772772-Les temps de réponse sont autour de 100 millisecondes, pour un débit de moins de 4000 requêtes par secondes, et 99% des réponses en moins de 135 millisecondes.
777777+Les temps de réponse sont autour de 100 millisecondes, pour un débit de moins de 4 000 requêtes par seconde, et 99 % des requêtes reçoivent une réponse en moins de 135 millisecondes.
773778774774-Avec la properties `spring.threads.virtual.enabled=true`, on obtient les performances suivantes, qui sont similaires aux performances de Tomcat 11 avec les Threads Virtuels :
779779+Avec la _properties_ `spring.threads.virtual.enabled=true`, on obtient les performances suivantes, qui sont similaires aux performances de Tomcat 11 avec les _Virtual Threads_ :
775780776781```
782782+$ hey -c 400 -n 1000000 http://localhost:8080
783783+777784Summary:
778785 Total: 126.7462 secs
779786 Slowest: 0.1738 secs
···808815 99% in 0.0539 secs
809816```
810817811811-Les temps de réponse sont autour de 50 millisecondes, pour un débit d'un peu moins de 8000 requêtes par seconde, et 99% des requêtes ont une réponse en moins de 53 millisecondes !
818818+Les temps de réponse sont autour de 50 millisecondes, pour un débit d'un peu moins de 8 000 requêtes par seconde, et 99 % des requêtes obtiennent une réponse en moins de 53 millisecondes !
812819813820## Conclusion
814821815815-Les résultats sont impressionnants. En utilisant le `VirtualThreadExecutor`, dans Tomcat 10.1 ou 11, on observe 10% de gains de performances sans rien faire de particulier, pour des Servlets n'effectuant aucun appel bloquant. Mais c'est vraiment à partir du moment où des appels bloquants sont effectués que les gains de performances sont les plus importants. Un Tomcat avec 200 _Threads Plateforme_ prend du temps à exécuter des _context switch_ qui ont un impact réel sur les temps de réponse. Ces impacts semblent purement annulés avec l'utilisation des _Threads Virtuels_.
822822+Les résultats sont impressionnants. En utilisant le `VirtualThreadExecutor`, dans Tomcat 11, on observe déjà 10 % de gains de performances sans rien faire de particulier, pour des Servlets n'effectuant pas d'appel bloquant.
823823+824824+Mais c'est vraiment à partir du moment où des appels bloquants sont effectués que les gains de performances sont les plus importants. Sur un Tomcat avec 200 _Threads_ plateforme, une fois les 200 _Threads_ bloqués, les autres requêtes sont mises en attente, ce qui occasionne des temps de réponse moyens plus longs. Ces impacts semblent purement annulés avec l'utilisation des _Virtual Threads_, puisque le nombre de _Threads_ n'est plus limité.
825825+Le débit théorique d'une application n'est maintenant plus limité par son nombre de _Threads_.
816826817817-Pour aller plus loin, l'utilisation des _Threads Virtuels_ dans Tomcat rendrait presque inutile l'utilisation des approches de programmation réactive. Le fait de rendre les Threads peu-coûteux à instancier, lié à leur mode d'exécution sur un Thread hôte, limite la charge déportée sur le système d'exploitation en _context switch_.
827827+Pour aller plus loin, l'utilisation des _Virtual Threads_ dans Tomcat rendrait presque inutile l'utilisation des approches de programmation réactive. Le fait de rendre les _Threads_ peu coûteux à instancier, lié à leur mode d'exécution sur un _Thread_hôte, limite la charge déportée sur le système d'exploitation en _context switches_, et maximise l'utilisation du CPU.
818828819829Il n'est maintenant plus problématique de bloquer un _Thread_.
820830821821-On peut déjà bénéficier de ces améliorations de performances avec Spring Boot 3, et Tomcat 10.1, à condition de bien utiliser une JVM 21. Donc pourquoi se priver ?
831831+On peut déjà bénéficier de ces améliorations de performances avec Spring Boot 3 et Tomcat 10.1, à condition de bien utiliser une JVM 21. Donc pourquoi se priver ?
822832823823-À suivre lors de la sortie future de Tomcat 11, quelle en sera l'intégration dans Spring Boot. Spring Boot ayant annoncé supporter Java 17 en version de base, la properties restera probablement toujours disponible, avec une valeur `false` par défaut.
833833+À suivre lors de la sortie future de Tomcat 11, quelle en sera l'intégration dans Spring Boot. Spring Boot ayant annoncé supporter Java 17 en version de base, la _properties_ `spring.threads.virtual.enabled` restera toujours disponible, avec probablement une valeur `false` par défaut.
824834825835## Liens et références
826836827837- [JEP 320](https://openjdk.org/jeps/320) - Suppression des modules Java EE et CORBA
828838- [JEP 444](https://openjdk.org/jeps/444) - _Virtual Threads_
829839- Documentation de [Tomcat](https://tomcat.apache.org/)
830830-- [RELEASE-NOTES Tomcat 11.0.0-M16](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M16/RELEASE-NOTES)
831831-- [Tomcat 11 Virtual Thread Implementation](https://tomcat.apache.org/tomcat-11.0-doc/config/executor.html#Virtual_Thread_Implementation) - Configuration des _Threads Virtuels_ dans Tomcat
832832-- [Virtual Threads](https://docs.spring.io/spring-boot/docs/3.2.2/reference/htmlsingle/#features.spring-application.virtual-threads) - Configuration des _Threads Virtuels_ dans Spring Boot
833833-- [Programmation Concurrente et Asynchrone : Loom en Java 20 et 21](https://www.youtube.com/watch?v=v7DzKOniNh0) - José Paumard
840840+- [RELEASE-NOTES Tomcat 11.0.0-M16](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M16/RELEASE-NOTES)
841841+- [Tomcat 11 Virtual Thread Implementation](https://tomcat.apache.org/tomcat-11.0-doc/config/executor.html#Virtual_Thread_Implementation) - Configuration des _Virtual Threads_ dans Tomcat
842842+- [Virtual Threads](https://docs.spring.io/spring-boot/docs/3.2.2/reference/htmlsingle/#features.spring-application.virtual-threads) - Configuration des _Virtual Threads_ dans Spring Boot
843843+- [Programmation Concurrente et Asynchrone : Loom en Java 20 et 21](https://www.youtube.com/watch?v=v7DzKOniNh0) - José Paumard
834844- [JMH](https://github.com/openjdk/jmh) : Java Microbenchmark Harness
845845+- [hey](https://github.com/rakyll/hey) : HTTP load generator, ApacheBench (ab) replacement