Source code of my website
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

✨ : add tomcat 11 article

+943
+35
content/posts/2024-02-05-tomcat-11-virtual-threads/class-diagram-mermaid.md
··· 1 + ```mermaid 2 + classDiagram 3 + namespace Tomcat { 4 + class Executor{ 5 + <<interface>> 6 + execute(runnable: Runnable) 7 + } 8 + class StandardThreadExecutor 9 + class StandardVirtualThreadExecutor 10 + 11 + class VirtualThreadExecutor { 12 + - threadBuilder: Thread.Builder 13 + } 14 + class ThreadPoolExecutor { 15 + - workQueue: BlockingQueue<Runnable> 16 + - workers: Set<Thead> 17 + } 18 + 19 + } 20 + 21 + namespace JDK { 22 + class AbstractExecutorService { 23 + execute(runnable: Runnable) 24 + } 25 + } 26 + 27 + Executor <|.. StandardThreadExecutor 28 + Executor <|.. StandardVirtualThreadExecutor 29 + 30 + StandardVirtualThreadExecutor --> VirtualThreadExecutor 31 + VirtualThreadExecutor ..|> AbstractExecutorService 32 + 33 + StandardThreadExecutor --> ThreadPoolExecutor 34 + ThreadPoolExecutor ..|> AbstractExecutorService 35 + ```
+833
content/posts/2024-02-05-tomcat-11-virtual-threads/index.md
··· 1 + --- 2 + created: "2024-02-05" 3 + date: "2024-02-05" 4 + language: fr 5 + tags: 6 + - Java 7 + title: Tomcat 11 & Virtual Threads 8 + --- 9 + 10 + ![](tomcat.jpg) 11 + 12 + _Apache Tomcat_ est le plus célèbre des conteneurs de _Servlets_ Java. 13 + 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. 14 + 15 + Chaque version majeure de _Tomcat_ apporte le support des nouvelles versions des API 'Java EE' ou 'JEE'. 16 + 17 + 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&nbsp;11_, dans laquelle suppression des packages `javax` liés à _Java EE_ a eu lieu. Les modules supprimés sont documentés dans la [JEP&nbsp;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. 18 + 19 + Les versions de _Tomcat_ sont donc à chaque fois compatibles avec une version minimale de Java, et des API `jakarta`. 20 + Le tableau ci-dessous reprend la liste des versions compatibles&nbsp;: 21 + 22 + | **Servlet Spec** | **Apache Tomcat Version** | **Supported Java Versions** | **Release date** | 23 + | ---------------- | ------------------------- | --------------------------- | ---------------- | 24 + | 6.1 | 11.0.x | 21 and later | (version alpha) | 25 + | 6.0 | 10.1.x | 11 and later | dec. 2020 | 26 + | 4.0 | 9.0.x | 8 and later | oct. 2017 | 27 + | 3.1 | 8.5.x | 7 and later | jan. 2014 | 28 + | 3.0 | 7.0.x (archived) | 6 and later | jan. 2011 | 29 + 30 + La version 11 de _Tomcat_ est donc dédiée à la version 21 de Java. 31 + Cette stratégie n'est pas surprenante en soit, la version 21 étant la dernière version LTS en date. 32 + 33 + Même si _Tomcat&nbsp;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&nbsp;11_ a été publiée en décembre 2022&nbsp;! 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)). 34 + 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)). 35 + 36 + La version actuelle est la _milestone_ 16, publiée le 9 janvier 2024. C'est cette version qui sera testée dans cet article. 37 + 38 + 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. 39 + 40 + ## C'est quoi les _Virtual Threads_&nbsp;? 41 + 42 + 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. 43 + 44 + 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. 45 + 46 + > 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. 47 + 48 + ### Les Threads _Plateforme_ 49 + 50 + 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. 51 + Ces 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. 52 + 53 + 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. 54 + 55 + #### Le coût en temps 56 + 57 + Le temps de création d'un _Thread_ dépend principalement du système d'exploitation et de sa charge actuelle. 58 + 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&nbsp;: 59 + 60 + ```java 61 + @BenchmarkMode(Mode.AverageTime) 62 + @OutputTimeUnit(TimeUnit.MILLISECONDS) 63 + public class ThreadsBenchmark { 64 + 65 + @Benchmark 66 + public void computeInMainThread(){ 67 + // un calcul quelconque 68 + Blackhole.consumeCPU(1024); 69 + } 70 + 71 + @Benchmark 72 + public void computeInPlatformThread() throws InterruptedException { 73 + // exécution dans un thread plateforme dédié 74 + var thread = Thread.ofPlatform().start(() -> { 75 + Blackhole.consumeCPU(1024); 76 + }); 77 + thread.join(); // attente de la fin de l'exécution 78 + } 79 + 80 + public static void main(String[] args) throws RunnerException { 81 + Options opt = new OptionsBuilder() 82 + .include(ThreadsBenchmark.class.getSimpleName()) 83 + .warmupIterations(1) // une itération de pré-chauffage de la JVM 84 + .measurementIterations(3) // 3 itérations de mesure 85 + .forks(1) 86 + .build(); 87 + 88 + new Runner(opt).run(); 89 + } 90 + 91 + } 92 + ``` 93 + 94 + 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. 95 + 96 + 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. 97 + 98 + Le résultat de l'exécution du benchmark est le suivant&nbsp;: 99 + 100 + ```bash 101 + Benchmark Mode Cnt Score Error Units 102 + ThreadsBenchmark.computeInMainThread avgt 3 0.002 ± 0.001 ms/op 103 + ThreadsBenchmark.computeInPlatformThread avgt 3 0.038 ± 0.015 ms/op 104 + ``` 105 + 106 + 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. 107 + 108 + > Créer un Thread pour effectuer un calcul peut donc être contre-productif&nbsp;! 109 + 110 + #### Le coût en mémoire 111 + 112 + 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. 113 + 114 + La commande suivante permet de constater les valeurs par défaut de la mémoire d'un _Thread_ Java&nbsp;: 115 + 116 + ```bash 117 + $ java -XX:+PrintFlagsFinal --version | grep -i ThreadStack 118 + 119 + intx CompilerThreadStackSize = 1024 {pd product} {default} 120 + intx ThreadStackSize = 1024 {pd product} {default} 121 + intx VMThreadStackSize = 1024 {pd product} {default} 122 + ``` 123 + 124 + La valeur est exprimé en kilo-octets. Un _Thread_ réservera donc 1024&nbsp;ko de RAM, soit 1&nbsp;Mo. 200&nbsp;_Threads_ réserveront donc 200&nbsp;Mo de RAM native, en plus de la RAM allouée à la _heap_ Java. 125 + 126 + ### Les Threads _Virtuels_ 127 + 128 + 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. 129 + 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é. 130 + 131 + 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&nbsp;: 132 + 133 + ```java 134 + @Benchmark 135 + public void computeInVirtualThread() throws InterruptedException { 136 + var thread = Thread.ofVirtual().start(() -> { 137 + Blackhole.consumeCPU(1024); 138 + }); 139 + thread.join(); 140 + } 141 + ``` 142 + 143 + Notez l'usage de `Thread.ofVirtual()` pour créer un _Thread Virtuel_ en lieu et place du `Thread.ofPlatform()`. 144 + 145 + Les durées d'exécution observées sont les suivantes&nbsp;: 146 + 147 + ```bash 148 + Benchmark Mode Cnt Score Error Units 149 + ThreadsBenchmark.computeInMainThread avgt 3 0.002 ± 0.001 ms/op 150 + ThreadsBenchmark.computeInPlatformThread avgt 3 0.037 ± 0.013 ms/op 151 + ThreadsBenchmark.computeInVirtualThread avgt 3 0.005 ± 0.002 ms/op 152 + ``` 153 + 154 + 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_. 155 + 156 + > Le coût d'exécution en temps d'un _Thread Virtuel_ est donc 15 fois inférieur à un Thread plateforme. 157 + 158 + 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. 159 + 160 + ## L'implémentation de Tomcat 161 + 162 + ![](tomcat-executors.png) 163 + 164 + 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. 165 + 166 + En farfouillant dans le code de Tomcat, on peut observer cette implémentation dans la classe `VirtualThreadExecutor`, qui est utilisée par le `StandardVirtualThreadExecutor`&nbsp;: 167 + 168 + ```java 169 + public class VirtualThreadExecutor extends AbstractExecutorService { 170 + 171 + private Thread.Builder threadBuilder; 172 + 173 + public VirtualThreadExecutor(String namePrefix) { 174 + threadBuilder = Thread.ofVirtual().name(namePrefix, 0); 175 + } 176 + 177 + @Override 178 + public void execute(Runnable command) { 179 + if (isShutdown()) { 180 + throw new RejectedExecutionException(); 181 + } 182 + threadBuilder.start(command); 183 + } 184 + } 185 + ``` 186 + 187 + > 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`. 188 + 189 + ## Le benchmark 190 + 191 + Dans cette section, nous allons tester les performances de deux versions de Tomcat&nbsp;: 192 + 193 + - la version 10.1, sans support des _Threads Virtuels_ 194 + - la version 11.0.0-M16, avec support des _Threads Virtuels_ activés 195 + 196 + 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)&nbsp;: 197 + 198 + ```bash 199 + java --version 200 + openjdk 21.0.1 2023-10-17 LTS 201 + OpenJDK Runtime Environment Temurin-21.0.1+12 (build 21.0.1+12-LTS) 202 + OpenJDK 64-Bit Server VM Temurin-21.0.1+12 (build 21.0.1+12-LTS, mixed mode, sharing) 203 + ``` 204 + 205 + J'ai aussi installé les version 10 et 11 de Tomcat&nbsp;: 206 + 207 + - la dernière version disponible de [Tomcat 10](https://tomcat.apache.org/download-10.cgi), la 10.1.18 208 + - la dernière version disponible de [Tomcat 11](https://tomcat.apache.org/download-11.cgi), la 11.0.0-M16 209 + 210 + 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 (!). 211 + 212 + Les JVM sont démarrées avec l'otion suivantes `-Xms512m -Xmx512m` pour positionner une taille de la heap à 512 Mo directement consommée. 213 + L'option `-XX:NativeMemoryTracking=summary` permet d'observer la consommation mémoire de la JVM. 214 + 215 + ```bash 216 + export CATALINA_OPTS='-Xms512m -Xmx512m -XX:NativeMemoryTracking=summary' 217 + ``` 218 + 219 + > 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. 220 + 221 + ### La configuration de Tomcat 11 222 + 223 + _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)&nbsp;: 224 + 225 + ```xml 226 + <Service name="Catalina"> 227 + 228 + <Executor 229 + name="virtualThreadsExecutor" 230 + className="org.apache.catalina.core.StandardVirtualThreadExecutor" /> 231 + 232 + 233 + <Connector executor="virtualThreadsExecutor" 234 + port="8080" protocol="HTTP/1.1" 235 + connectionTimeout="20000" 236 + redirectPort="8443" /> 237 + 238 + ... 239 + 240 + </Service> 241 + ``` 242 + 243 + On paramètre donc le `StandardVirtualThreadExecutor` comme devant traiter les requêtes allouées au _Connector_ écoutant sur le port `8080`. 244 + 245 + Aucune autre configuration n'est nécessaire. Aucune configuration particulière n'est fait sur le Tomcat 10.1. 246 + 247 + ### Les perfs attendues 248 + 249 + On s'attend, entre Tomcat&nbsp;10.1 et Tomcat&nbsp;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. 250 + En principe, les _Threads Virtuels_ ne devraient utiliser que quelques _Threads Plateforme_, et donc limiter les _context switch_ en cas de charge importante. 251 + 252 + ### Démarrage et empreinte mémoire à vide 253 + 254 + #### Tomcat 10.1 255 + 256 + Le Tomcat 10.1 est démarré avec la commande `startup.sh`&nbsp;: 257 + 258 + ```bash 259 + ./startup.sh 260 + Using CATALINA_BASE: /opt/apache-tomcat-10.1.18 261 + Using CATALINA_HOME: /opt/apache-tomcat-10.1.18 262 + Using CATALINA_TMPDIR: /opt/apache-tomcat-10.1.18/temp 263 + Using JRE_HOME: /opt/jdk-21.0.2+13 264 + Using CLASSPATH: /opt/apache-tomcat-10.1.18/bin/bootstrap.jar:/opt/apache-tomcat-10.1.18/bin/tomcat-juli.jar 265 + Using CATALINA_OPTS: -Xms512m -Xmx512m -XX:NativeMemoryTracking=summary 266 + Tomcat started. 267 + ``` 268 + 269 + L'empreinte mémoire de notre _Tomcat_ se fait avec la séquence de commandes&nbsp;: 270 + 271 + ```bash 272 + # listing des JVM en cours d'exécution 273 + $ jps -l 274 + 275 + # récupération directe de l'identifiant lié à Tomcat 276 + $ jps -l | grep -v 'jps' | cut -d ' ' -f 1 277 + # récupération de l'empreinte mémoire 278 + $ jcmd $(jps -l | grep -v 'jps' | cut -d ' ' -f 1) VM.native_memory 279 + 280 + Native Memory Tracking: 281 + 282 + (Omitting categories weighting less than 1KB) 283 + 284 + Total: reserved=2014475KB, committed=635935KB 285 + malloc: 26831KB #72747 286 + mmap: reserved=1987644KB, committed=609104KB 287 + 288 + - Java Heap (reserved=524288KB, committed=524288KB) 289 + (mmap: reserved=524288KB, committed=524288KB) 290 + 291 + - Thread (reserved=42108KB, committed=2792KB) 292 + (thread #41) 293 + (stack: reserved=41984KB, committed=2668KB) 294 + (malloc=78KB #251) (peak=89KB #261) 295 + (arena=46KB #80) (peak=317KB #52) 296 + 297 + ``` 298 + 299 + On observe que notre Heap est bien réservée à 512&nbsp;Mo (524288KB), et que 41&nbsp;_Threads_ ont été démarrés (dont les 25&nbsp;_Threads_ liés à notre `Executor`), pour une consommation de 41&nbsp;Mo supplémentaires. Nous avons un total de mémoire consommée de près de 630&nbsp;Mo, car d'autres espaces sont réservés par la JVM (espaces de code, etc...). 300 + 301 + 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&nbsp;_Threads_. 302 + 303 + ```bash 304 + $ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample 305 + 306 + $ jcmd $(jps -l | grep -v 'jps' | cut -d ' ' -f 1) VM.native_memory 307 + 308 + Native Memory Tracking: 309 + 310 + (Omitting categories weighting less than 1KB) 311 + 312 + Total: reserved=2214510KB, committed=671762KB 313 + malloc: 32306KB #93366 314 + mmap: reserved=2182204KB, committed=639456KB 315 + 316 + - Java Heap (reserved=524288KB, committed=524288KB) 317 + (mmap: reserved=524288KB, committed=524288KB) 318 + 319 + - Thread (reserved=237307KB, committed=24319KB) 320 + (thread #231) 321 + (stack: reserved=236544KB, committed=23556KB) 322 + (malloc=494KB #1403) (peak=506KB #1413) 323 + (arena=269KB #460) (peak=317KB #52) 324 + ``` 325 + 326 + 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_. 327 + 328 + #### Tomcat 11 329 + 330 + Comme pour le Tomcat 10.1, le Tomcat 11 est démarré&nbsp;: 331 + 332 + ```bash 333 + $ ./bin/startup.sh 334 + Using CATALINA_BASE: /opt/apache-tomcat-11.0.0-M16 335 + Using CATALINA_HOME: /opt/apache-tomcat-11.0.0-M16 336 + Using CATALINA_TMPDIR: /opt/apache-tomcat-11.0.0-M16/temp 337 + Using JRE_HOME: /opt/jdk-21.0.2+13 338 + Using CLASSPATH: /opt/apache-tomcat-11.0.0-M16/bin/bootstrap.jar:/opt/apache-tomcat-11.0.0-M16/bin/tomcat-juli.jar 339 + Using CATALINA_OPTS: -Xms512m -Xmx512m -XX:NativeMemoryTracking=summary 340 + Tomcat started. 341 + ``` 342 + 343 + La consommation mémoire observée&nbsp;: 344 + 345 + ```bash 346 + $ jcmd $(jps -l | grep -v 'jps' | cut -d ' ' -f 1) VM.native_memory 347 + 348 + Native Memory Tracking: 349 + 350 + (Omitting categories weighting less than 1KB) 351 + 352 + Total: reserved=2004350KB, committed=635010KB 353 + malloc: 26946KB #72371 354 + mmap: reserved=1977404KB, committed=608064KB 355 + 356 + - Java Heap (reserved=524288KB, committed=524288KB) 357 + (mmap: reserved=524288KB, committed=524288KB) 358 + 359 + - Thread (reserved=31835KB, committed=1719KB) 360 + (thread #31) 361 + (stack: reserved=31744KB, committed=1628KB) 362 + (malloc=57KB #191) (peak=67KB #201) 363 + (arena=34KB #60) (peak=317KB #52) 364 + ``` 365 + 366 + 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. 367 + 368 + Après avoir passé une charge identique au test du Tomcat 10.1&nbsp;: 369 + 370 + ```bash 371 + $ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample 372 + 373 + $ jcmd $(jps -l | grep -v 'jps' | cut -d ' ' -f 1) VM.native_memory 374 + 375 + Native Memory Tracking: 376 + 377 + (Omitting categories weighting less than 1KB) 378 + 379 + Total: reserved=2022976KB, committed=655120KB 380 + malloc: 37380KB #88191 381 + mmap: reserved=1985596KB, committed=617740KB 382 + 383 + - Java Heap (reserved=524288KB, committed=524288KB) 384 + (mmap: reserved=524288KB, committed=524288KB) 385 + 386 + - Thread (reserved=40054KB, committed=2798KB) 387 + (thread #39) 388 + (stack: reserved=39936KB, committed=2680KB) 389 + (malloc=74KB #239) (peak=87KB #255) 390 + (arena=44KB #76) (peak=317KB #52) 391 + 392 + ``` 393 + 394 + On observe que Tomcat a instancié quelques _Threads_ en plus, pour passer à 39 et on atteint donc les 39&nbsp;Mo de stack allouée. 395 + On économise donc pas loin de 200 Mo comme attendu. 396 + 397 + > 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. 398 + 399 + 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. 400 + 401 + ### Performances avec une Servlet simple 402 + 403 + 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. 404 + 405 + > 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. 406 + 407 + Je requête la servlet `HelloWorldExample`, qui est fournie avec Tomcat. Cette servlet affiche simplement une page web contenant le message _Hello World_. 408 + 409 + #### Tomcat 10.1 - Threads Plateforme 410 + 411 + ``` 412 + hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample 413 + 414 + Summary: 415 + Total: 8.4899 secs 416 + Slowest: 0.0997 secs 417 + Fastest: 0.0000 secs 418 + Average: 0.0034 secs 419 + Requests/sec: 117787.3647 420 + 421 + Total data: 387000000 bytes 422 + Size/request: 387 bytes 423 + 424 + Response time histogram: 425 + 0.000 [1] | 426 + 0.010 [991307] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 427 + 0.020 [8096] | 428 + 0.030 [167] | 429 + 0.040 [16] | 430 + 0.050 [22] | 431 + 0.060 [5] | 432 + 0.070 [0] | 433 + 0.080 [223] | 434 + 0.090 [129] | 435 + 0.100 [34] | 436 + 437 + 438 + Latency distribution: 439 + 10% in 0.0017 secs 440 + 25% in 0.0023 secs 441 + 50% in 0.0030 secs 442 + 75% in 0.0040 secs 443 + 90% in 0.0054 secs 444 + 95% in 0.0066 secs 445 + 99% in 0.0097 secs 446 + 447 + ``` 448 + 449 + 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. 450 + 451 + #### Tomcat 11 - Threads Virtuels 452 + 453 + Le même test a été lancé sur Tomcat 11 configuré avec des _Threads Virtuels_&nbsp;: 454 + 455 + ``` 456 + hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/HelloWorldExample 457 + 458 + Summary: 459 + Total: 7.7188 secs 460 + Slowest: 0.1194 secs 461 + Fastest: 0.0000 secs 462 + Average: 0.0031 secs 463 + Requests/sec: 129554.4854 464 + 465 + Total data: 387000000 bytes 466 + Size/request: 387 bytes 467 + 468 + Response time histogram: 469 + 0.000 [1] | 470 + 0.012 [998863] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 471 + 0.024 [588] | 472 + 0.036 [138] | 473 + 0.048 [10] | 474 + 0.060 [0] | 475 + 0.072 [102] | 476 + 0.084 [245] | 477 + 0.096 [13] | 478 + 0.107 [36] | 479 + 0.119 [4] | 480 + 481 + 482 + Latency distribution: 483 + 10% in 0.0015 secs 484 + 25% in 0.0021 secs 485 + 50% in 0.0028 secs 486 + 75% in 0.0037 secs 487 + 90% in 0.0048 secs 488 + 95% in 0.0056 secs 489 + 99% in 0.0080 secs 490 + ``` 491 + 492 + Le temps moyen d'exécution est de 3,1&nbsp;millisecondes, et 99% des réponses ont été données en moins de 9&nbsp;millisecondes. 493 + On a une amélioration de performances de près de 10% pour une simple servlet&nbsp;! 494 + 495 + 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&nbsp;Threads en paralèlle dans le cas de Tomcat&nbsp;11. Ce qui occasionne donc plus de temps disponible, et donc des meilleurs temps de réponse. 496 + 497 + ### Performances avec une Servlet effectuant un appel bloquant 498 + 499 + 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&nbsp;: `Thread.sleep(50)`&nbsp;: 500 + 501 + ```java 502 + public class ThreadInfo extends HttpServlet { 503 + 504 + @Override 505 + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { 506 + response.setContentType("text/html"); 507 + response.setCharacterEncoding("UTF-8"); 508 + 509 + PrintWriter out = response.getWriter(); 510 + out.println("<!DOCTYPE html><html>"); 511 + out.println("<head>"); 512 + out.println("<meta charset=\"UTF-8\" />"); 513 + 514 + out.println("<title>Thread info</title>"); 515 + out.println("</head>"); 516 + out.println("<body><h1>" + Thread.currentThread().getName() + "</h1></body>"); 517 + 518 + try { 519 + Thread.sleep(50L); // fais dodo 520 + } catch (InterruptedException ex) { 521 + } 522 + } 523 + 524 + @Override 525 + public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { 526 + doGet(request, response); 527 + } 528 + 529 + } 530 + ``` 531 + 532 + Quel est l'impact attendu&nbsp;? 533 + Pour Tomcat 10.1, qui dispose de 200&nbsp;_Threads_ maximum, on s'attend à obtenir un débit de 4000&nbsp;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 ). 534 + Pour Tomcat 11, non limité par des _Threads_, on s'attend à obtenir un débit similaire au test précédent. 535 + 536 + #### Tomcat 10.1 - Threads Plateforme - Appels Bloquants 537 + 538 + Le tir de performances sur le Tomcat 10.1 donne le résultat suivant&nbsp;: 539 + 540 + ``` 541 + $ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/ThreadInfo 542 + 543 + Summary: 544 + Total: 252.0313 secs 545 + Slowest: 0.1661 secs 546 + Fastest: 0.0501 secs 547 + Average: 0.1006 secs 548 + Requests/sec: 3967.7610 549 + 550 + Total data: 133460003 bytes 551 + Size/request: 133 bytes 552 + 553 + Response time histogram: 554 + 0.050 [1] | 555 + 0.062 [24721] |■ 556 + 0.073 [73] | 557 + 0.085 [12] | 558 + 0.097 [3051] | 559 + 0.108 [943320] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 560 + 0.120 [3356] | 561 + 0.131 [4993] | 562 + 0.143 [4193] | 563 + 0.155 [16063] |■ 564 + 0.166 [217] | 565 + 566 + 567 + Latency distribution: 568 + 10% in 0.1002 secs 569 + 25% in 0.1004 secs 570 + 50% in 0.1006 secs 571 + 75% in 0.1010 secs 572 + 90% in 0.1016 secs 573 + 95% in 0.1024 secs 574 + 99% in 0.1451 secs 575 + ``` 576 + 577 + 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. 578 + 579 + 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&nbsps;threads bloqués et occupés pendant 50&nbsp;millisecondes chacun. 580 + 581 + #### Tomcat 11 582 + 583 + Le même tir de performances sur Tomcat&nbsp;11 configuré avec les _Threads Virtuels_ donne un résultat complètement différent&nbsp;: 584 + 585 + ``` 586 + $ hey -c 400 -n 1000000 http://localhost:8080/examples/servlets/servlet/ThreadInfo 587 + 588 + Summary: 589 + Total: 126.8828 secs 590 + Slowest: 0.3636 secs 591 + Fastest: 0.0501 secs 592 + Average: 0.0507 secs 593 + Requests/sec: 7881.2884 594 + 595 + Total data: 129884989 bytes 596 + Size/request: 129 bytes 597 + 598 + Response time histogram: 599 + 0.050 [1] | 600 + 0.081 [999544] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 601 + 0.113 [115] | 602 + 0.144 [93] | 603 + 0.175 [114] | 604 + 0.207 [132] | 605 + 0.238 [0] | 606 + 0.270 [0] | 607 + 0.301 [0] | 608 + 0.332 [0] | 609 + 0.364 [1] | 610 + 611 + 612 + Latency distribution: 613 + 10% in 0.0502 secs 614 + 25% in 0.0503 secs 615 + 50% in 0.0504 secs 616 + 75% in 0.0507 secs 617 + 90% in 0.0512 secs 618 + 95% in 0.0517 secs 619 + 99% in 0.0544 secs 620 + ``` 621 + 622 + On observe que le temps moyen de réponse à une requête est bien de 50&nbsp;millisecondes. Aucune surcharge liée à du _context switch_ n'est observée ici. 99% des requêtes sont répondues en 54 millisecondes. 623 + 624 + 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. 625 + 626 + Un test rapide permet d'augmenter le nombre de workers à 1000 pour observer la différence&nbsp;: 627 + 628 + ``` 629 + $ hey -c 1000 -n 1000000 http://localhost:8080/examples/servlets/servlet/ThreadInfo 630 + 631 + Summary: 632 + Total: 50.8318 secs 633 + Slowest: 0.2331 secs 634 + Fastest: 0.0501 secs 635 + Average: 0.0507 secs 636 + Requests/sec: 19672.7068 637 + 638 + Total data: 128987581 bytes 639 + Size/request: 130 bytes 640 + 641 + Response time histogram: 642 + 0.050 [1] | 643 + 0.068 [983411] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 644 + 0.087 [277] | 645 + 0.105 [59] | 646 + 0.123 [28] | 647 + 0.142 [27] | 648 + 0.160 [0] | 649 + 0.178 [124] | 650 + 0.197 [271] | 651 + 0.215 [309] | 652 + 0.233 [205] | 653 + 654 + 655 + Latency distribution: 656 + 10% in 0.0501 secs 657 + 25% in 0.0502 secs 658 + 50% in 0.0502 secs 659 + 75% in 0.0504 secs 660 + 90% in 0.0512 secs 661 + 95% in 0.0523 secs 662 + 99% in 0.0568 secs 663 + ``` 664 + 665 + Avec 1000 workers, le temps moyen de réponse reste autour de 50&nbsp;millisecondes. 99% des requêtes reçoivent une réponse en moins de 58 millisecondes. Le débit passe à 1900 requêtes par seconde&nbsp;! 666 + 667 + On atteint malheureusement ici les limites de ma machine, puisque à ce stade quelques erreurs sont observées&nbsp;: `dial tcp 127.0.0.1:8080: socket: too many open files`. 668 + 669 + Cependant, ces performances laissent deviner qu'il serait possible d'aller encore plus loin. 670 + 671 + ## Bonus, avec Spring Boot 3 672 + 673 + > "Julien, tu es bien gentil avec tes Servlets, mais plus personne n'en développe." 674 + 675 + Cette partie "bonus" teste le même comportement, mais avec Spring Boot 3&nbsp;! 676 + 677 + ### Configurer Spring Boot 3 678 + 679 + Malheureusement, il n'est pas possible pour le moment d'utiliser Tomcat 11 avec Spring Boot 3. 680 + Néanmoins, Spring Boot 3 a intégré le support des _Threads Virtuels_ et de l'Exécutor Tomcat à Tomcat 10&nbsp;! 681 + 682 + Pour utiliser les Virtual Threads dans Spring Boot 3, il faut positionner la properties suivante&nbsp;: 683 + 684 + ```properties 685 + spring.threads.virtual.enabled=true 686 + ``` 687 + 688 + Côté Spring Boot, cette properties est interprétée par l'annotation `@ConditionalOnThreading` et configure un `TomcatVirtualThreadsWebServerFactoryCustomizer`&nbsp;: 689 + 690 + ```java 691 + @Configuration(proxyBeanMethods = false) 692 + @ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class }) 693 + public static class TomcatWebServerFactoryCustomizerConfiguration { 694 + 695 + @Bean 696 + @ConditionalOnThreading(Threading.VIRTUAL) 697 + TomcatVirtualThreadsWebServerFactoryCustomizer tomcatVirtualThreadsProtocolHandlerCustomizer() { 698 + return new TomcatVirtualThreadsWebServerFactoryCustomizer(); 699 + } 700 + 701 + } 702 + ``` 703 + 704 + Le `TomcatVirtualThreadsWebServerFactoryCustomizer` configure le Tomcat embedded pour utiliser l'executor `VirtualThreadExecutor`&nbsp;: 705 + 706 + ```java 707 + public class TomcatVirtualThreadsWebServerFactoryCustomizer 708 + implements WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory>, Ordered { 709 + 710 + @Override 711 + public void customize(ConfigurableTomcatWebServerFactory factory) { 712 + factory.addProtocolHandlerCustomizers( 713 + (protocolHandler) -> protocolHandler.setExecutor(new VirtualThreadExecutor("tomcat-handler-"))); 714 + } 715 + } 716 + ``` 717 + 718 + 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&nbsp;: 719 + 720 + ```java 721 + @RestController 722 + public class ThreadController { 723 + 724 + @GetMapping("/") 725 + String getThreadName() throws InterruptedException { 726 + Thread.sleep(50L); // gros dodo 727 + return Thread.currentThread().getName(); 728 + } 729 + } 730 + ``` 731 + 732 + 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&nbsp;: 733 + 734 + ``` 735 + hey -c 400 -n 1000000 http://localhost:8080 736 + 737 + Summary: 738 + Total: 253.9172 secs 739 + Slowest: 0.2498 secs 740 + Fastest: 0.0501 secs 741 + Average: 0.1013 secs 742 + Requests/sec: 3938.2910 743 + 744 + Total data: 21459960 bytes 745 + Size/request: 21 bytes 746 + 747 + Response time histogram: 748 + 0.050 [1] | 749 + 0.070 [22735] |■ 750 + 0.090 [353] | 751 + 0.110 [949895] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 752 + 0.130 [7069] | 753 + 0.150 [19591] |■ 754 + 0.170 [67] | 755 + 0.190 [74] | 756 + 0.210 [99] | 757 + 0.230 [46] | 758 + 0.250 [70] | 759 + 760 + 761 + Latency distribution: 762 + 10% in 0.1004 secs 763 + 25% in 0.1008 secs 764 + 50% in 0.1014 secs 765 + 75% in 0.1021 secs 766 + 90% in 0.1036 secs 767 + 95% in 0.1058 secs 768 + 99% in 0.1352 secs 769 + ``` 770 + 771 + 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. 772 + 773 + 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&nbsp;: 774 + 775 + ``` 776 + Summary: 777 + Total: 126.7462 secs 778 + Slowest: 0.1738 secs 779 + Fastest: 0.0501 secs 780 + Average: 0.0507 secs 781 + Requests/sec: 7889.7847 782 + 783 + Total data: 20941836 bytes 784 + Size/request: 20 bytes 785 + 786 + Response time histogram: 787 + 0.050 [1] | 788 + 0.062 [999571] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 789 + 0.075 [15] | 790 + 0.087 [68] | 791 + 0.100 [0] | 792 + 0.112 [65] | 793 + 0.124 [0] | 794 + 0.137 [61] | 795 + 0.149 [57] | 796 + 0.161 [16] | 797 + 0.174 [146] | 798 + 799 + 800 + Latency distribution: 801 + 10% in 0.0502 secs 802 + 25% in 0.0503 secs 803 + 50% in 0.0504 secs 804 + 75% in 0.0507 secs 805 + 90% in 0.0513 secs 806 + 95% in 0.0519 secs 807 + 99% in 0.0539 secs 808 + ``` 809 + 810 + 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&nbsp;! 811 + 812 + ## Conclusion 813 + 814 + 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_. 815 + 816 + 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_. 817 + 818 + Il n'est maintenant plus problématique de bloquer un _Thread_. 819 + 820 + 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&nbsp;? 821 + 822 + À 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. 823 + 824 + ## Liens et références 825 + 826 + - [JEP&nbsp;320](https://openjdk.org/jeps/320) - Suppression des modules Java EE et CORBA 827 + - [JEP&nbsp;444](https://openjdk.org/jeps/444) - _Virtual Threads_ 828 + - Documentation de [Tomcat](https://tomcat.apache.org/) 829 + - [RELEASE-NOTES Tomcat 11.0.0-M16](https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0-M16/RELEASE-NOTES) 830 + - [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 831 + - [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 832 + - [Programmation Concurrente et Asynchrone&nbsp;: Loom en Java 20 et 21](https://www.youtube.com/watch?v=v7DzKOniNh0) - José Paumard 833 + - [JMH](https://github.com/openjdk/jmh)&nbsp;: Java Microbenchmark Harness
+75
content/posts/2024-02-05-tomcat-11-virtual-threads/tomcat-executors.drawio
··· 1 + <mxfile host="Electron" modified="2024-02-05T14:29:46.846Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.1.18 Chrome/120.0.6099.199 Electron/28.1.2 Safari/537.36" etag="K0WJKA20SPJnUfvGFyHI" version="22.1.18" type="device"> 2 + <diagram name="Page-1" id="KB8BvCnyQ-29_4Z_fe-B"> 3 + <mxGraphModel dx="782" dy="1118" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0"> 4 + <root> 5 + <mxCell id="0" /> 6 + <mxCell id="1" parent="0" /> 7 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-18" value="org.apache.tomcat.util.threads" style="shape=folder;fontStyle=1;spacingTop=10;tabWidth=40;tabHeight=14;tabPosition=left;html=1;whiteSpace=wrap;verticalAlign=top;" vertex="1" parent="1"> 8 + <mxGeometry x="80" y="360" width="560" height="160" as="geometry" /> 9 + </mxCell> 10 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-15" value="org.apache.catalina.core" style="shape=folder;fontStyle=1;spacingTop=10;tabWidth=40;tabHeight=14;tabPosition=left;html=1;whiteSpace=wrap;verticalAlign=top;" vertex="1" parent="1"> 11 + <mxGeometry x="80" y="40" width="540" height="290" as="geometry" /> 12 + </mxCell> 13 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-1" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;&lt;i&gt;&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;/i&gt;&lt;br&gt;&lt;b&gt;Executor&lt;/b&gt;&lt;/p&gt;&lt;hr size=&quot;1&quot;&gt;&lt;p style=&quot;margin:0px;margin-left:4px;&quot;&gt;&lt;/p&gt;&lt;p style=&quot;margin:0px;margin-left:4px;&quot;&gt;+ execute(runnable: Runnable)&lt;/p&gt;" style="verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;whiteSpace=wrap;" vertex="1" parent="1"> 14 + <mxGeometry x="240" y="80" width="200" height="80" as="geometry" /> 15 + </mxCell> 16 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;dashPattern=8 8;" edge="1" parent="1" source="O4fw5JElKIW0x6O_YH6Z-2" target="O4fw5JElKIW0x6O_YH6Z-1"> 17 + <mxGeometry relative="1" as="geometry" /> 18 + </mxCell> 19 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="O4fw5JElKIW0x6O_YH6Z-2" target="O4fw5JElKIW0x6O_YH6Z-6"> 20 + <mxGeometry relative="1" as="geometry" /> 21 + </mxCell> 22 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-2" value="StandardVirtualThreadExecutor" style="html=1;whiteSpace=wrap;" vertex="1" parent="1"> 23 + <mxGeometry x="120" y="240" width="200" height="40" as="geometry" /> 24 + </mxCell> 25 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;dashPattern=8 8;endArrow=block;endFill=0;" edge="1" parent="1" source="O4fw5JElKIW0x6O_YH6Z-3" target="O4fw5JElKIW0x6O_YH6Z-1"> 26 + <mxGeometry relative="1" as="geometry" /> 27 + </mxCell> 28 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="O4fw5JElKIW0x6O_YH6Z-3" target="O4fw5JElKIW0x6O_YH6Z-12"> 29 + <mxGeometry relative="1" as="geometry" /> 30 + </mxCell> 31 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-3" value="StandardThreadExecutor" style="html=1;whiteSpace=wrap;" vertex="1" parent="1"> 32 + <mxGeometry x="360" y="240" width="200" height="40" as="geometry" /> 33 + </mxCell> 34 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-6" value="VirtualThreadExecutor" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> 35 + <mxGeometry x="120" y="400" width="200" height="80" as="geometry" /> 36 + </mxCell> 37 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-7" value="- threadBuilder: Thread.Builder" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="O4fw5JElKIW0x6O_YH6Z-6"> 38 + <mxGeometry y="26" width="200" height="54" as="geometry" /> 39 + </mxCell> 40 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-12" value="ThreadPoolExecutor" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> 41 + <mxGeometry x="360" y="400" width="200" height="80" as="geometry" /> 42 + </mxCell> 43 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-13" value="-workQueue: BlockingQueue&lt;br&gt;-workers: Set&amp;lt;Thread&amp;gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;" vertex="1" parent="O4fw5JElKIW0x6O_YH6Z-12"> 44 + <mxGeometry y="26" width="200" height="54" as="geometry" /> 45 + </mxCell> 46 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-16" value="java.util.concurrent" style="shape=folder;fontStyle=1;spacingTop=10;tabWidth=40;tabHeight=14;tabPosition=left;html=1;whiteSpace=wrap;verticalAlign=top;" vertex="1" parent="1"> 47 + <mxGeometry x="200" y="600" width="280" height="180" as="geometry" /> 48 + </mxCell> 49 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-17" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;&lt;i&gt;&amp;lt;&amp;lt;abstract&amp;gt;&amp;gt;&lt;/i&gt;&lt;br&gt;&lt;b&gt;AbstractExecutorService&lt;/b&gt;&lt;/p&gt;&lt;hr size=&quot;1&quot;&gt;&lt;p style=&quot;margin:0px;margin-left:4px;&quot;&gt;&lt;/p&gt;&lt;p style=&quot;margin:0px;margin-left:4px;&quot;&gt;+ execute(runnable: Runnable)&lt;/p&gt;" style="verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;whiteSpace=wrap;" vertex="1" parent="1"> 50 + <mxGeometry x="240" y="660" width="202.5" height="80" as="geometry" /> 51 + </mxCell> 52 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;dashPattern=8 8;" edge="1" parent="1" source="O4fw5JElKIW0x6O_YH6Z-6" target="O4fw5JElKIW0x6O_YH6Z-17"> 53 + <mxGeometry relative="1" as="geometry"> 54 + <mxPoint x="230" y="250" as="sourcePoint" /> 55 + <mxPoint x="350" y="170" as="targetPoint" /> 56 + <Array as="points"> 57 + <mxPoint x="220" y="560" /> 58 + <mxPoint x="341" y="560" /> 59 + </Array> 60 + </mxGeometry> 61 + </mxCell> 62 + <mxCell id="O4fw5JElKIW0x6O_YH6Z-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;dashPattern=8 8;" edge="1" parent="1" source="O4fw5JElKIW0x6O_YH6Z-13" target="O4fw5JElKIW0x6O_YH6Z-17"> 63 + <mxGeometry relative="1" as="geometry"> 64 + <mxPoint x="230" y="462" as="sourcePoint" /> 65 + <mxPoint x="351" y="630" as="targetPoint" /> 66 + <Array as="points"> 67 + <mxPoint x="450" y="560" /> 68 + <mxPoint x="341" y="560" /> 69 + </Array> 70 + </mxGeometry> 71 + </mxCell> 72 + </root> 73 + </mxGraphModel> 74 + </diagram> 75 + </mxfile>
content/posts/2024-02-05-tomcat-11-virtual-threads/tomcat-executors.png

This is a binary file and will not be displayed.

content/posts/2024-02-05-tomcat-11-virtual-threads/tomcat.jpg

This is a binary file and will not be displayed.