1. Scalabilité
Pourquoi les threads classiques passent mal à l'échelle?
Chaque thread classique (Thread) est une entité du système d'exploitation avec sa propre pile (généralement 1–2 Mo) et une structure d'état. Tenter de créer, disons, 10 000 threads classiques conduit souvent à une erreur OutOfMemoryError. C'est pourquoi les serveurs traditionnels utilisent des pools de threads limités.
Threads virtuels: la magie de la scalabilité
Les threads virtuels (Java 21+) sont des threads « légers » gérés par la JVM. Leur pile est stockée dans le tas (heap) et peut croître/se réduire dynamiquement. Lorsqu'un thread se bloque sur l'I/O, la JVM le « met en pause » et poursuit l'exécution d'autres tâches.
À l'intérieur de la JVM, un petit pool de « threads porteurs » (carrier threads) — des threads de plateforme de l'OS — exécute à tour de rôle les threads virtuels. Cela permet de créer 100 000+ tâches sans catastrophe mémoire. La JVM planifie elle‑même quels threads virtuels exécuter et quand.
Démonstration: 100_000 threads virtuels contre 1_000 threads de plateforme
Exemple: création de 1000 threads classiques
// Tentative de créer 1000 threads classiques
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
Thread t = new Thread(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threads.add(t);
t.start();
}
System.out.println("Threads créés: " + threads.size());
Résultat: Sur la plupart des systèmes, on parvient à créer 1 000–2 000 threads; au-delà, les problèmes de mémoire et les ralentissements apparaissent.
Exemple: création de 100 000 threads virtuels
// Nous créons 100_000 threads virtuels
List<Thread> vThreads = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
Thread t = Thread.ofVirtual().start(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
vThreads.add(t);
}
System.out.println("Threads virtuels créés: " + vThreads.size());
Résultat: Le programme crée sans problème 100 000 threads virtuels sans plantages ni ralentissements significatifs. La mémoire requise est largement inférieure.
Comparaison visuelle
| Type de thread | Threads maximum (environ) | Utilisation mémoire | Temps de démarrage |
|---|---|---|---|
| Classiques (Thread) | 1 000 – 10 000 | Élevée | Long |
| Virtuels | 100 000 – 1 000 000+ | Faible | Instantané |
Fait: Les threads virtuels permettent d'écrire du code « un thread par tâche » sans pools complexes ni risque de surcharge du système.
2. Performances: où les threads virtuels excellent
Tâches limitées par l'I/O (I/O-bound)
Les threads virtuels conviennent parfaitement aux requêtes réseau, à l'I/O fichier et au travail avec les bases de données. Quand une opération est bloquante, le thread virtuel libère le « thread porteur », et la JVM exécute d'autres tâches. Cela augmente le débit lorsqu'il y a un grand nombre d'attentes simultanées.
Exemple: simulation de 10 000 requêtes HTTP simultanées
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
Thread t = Thread.ofVirtual().start(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Réponse: " + response.statusCode());
} catch (Exception e) {
System.out.println("Erreur: " + e.getMessage());
}
});
threads.add(t);
}
// On attend la fin de tous les threads
for (Thread t : threads) {
t.join();
}
Résultat: Les 10 000 requêtes s'exécutent en parallèle; le programme ne plante pas et le code reste simple.
CPU-bound: les threads virtuels n'accélèrent pas les calculs
Si la tâche sollicite le CPU, les threads virtuels n'apporteront pas de vitesse: le nombre de cœurs est fixé. Ici, des pools fixes, égaux au nombre de cœurs, sont appropriés pour éviter une concurrence inutile.
// Chaque tâche calcule la somme d'un grand intervalle
Runnable cpuTask = () -> {
long sum = 0;
for (int i = 0; i < 100_000_000; i++) {
sum += i;
}
System.out.println("Somme: " + sum);
};
// Lancement de 1000 threads virtuels avec des calculs
for (int i = 0; i < 1000; i++) {
Thread.ofVirtual().start(cpuTask);
}
Résultat: Les threads seront en concurrence pour le CPU, mais il n'y aura pas d'accélération — ce n'est pas une tâche pour Virtual Threads.
3. Limitations et particularités
Synchronisation et pièges des threads virtuels
- Attention aux verrous natifs. L'utilisation de synchronized peut « coller » un thread virtuel à son porteur, réduisant les bénéfices. Préférez ReentrantLock, Semaphore et d'autres primitives de java.util.concurrent optimisées pour les threads virtuels.
- Anciennes bibliothèques. Certains pilotes JDBC et bibliothèques natives ne sont pas encore optimisés pour Virtual Threads. Testez soigneusement les opérations bloquantes.
Pas pour les tâches de longue durée
Virtual Threads sont idéaux pour des unités de travail « courtes »: traitement d'une requête, une opération puis terminaison. Un million de tâches qui vivent indéfiniment (par exemple des calculs sans fin) n'apportera rien — utilisez pour elles des threads de plateforme.
4. Bonnes pratiques: où utiliser les threads virtuels
- Tâches I/O‑bound: appels réseau, fichiers, bases de données — partout où un thread attend souvent.
- Serveurs Web: traitez chaque requête HTTP dans un thread virtuel distinct.
- Tests d'intégration: simulez rapidement des milliers de clients.
- Traitement asynchrone: écrivez le code « bloquant » habituel — la JVM effectue une planification intelligente.
À éviter:
- Pour des tâches qui sollicitent constamment le CPU.
- Lorsque la compatibilité avec des bibliothèques bas niveau est critique (toutes ne sont pas encore adaptées).
Sous le capot, il est pratique d'utiliser l'exécuteur: Executors.newVirtualThreadPerTaskExecutor() — « un thread virtuel par tâche », sans pool fixe.
5. Surveillance et mesure: comment voir les threads virtuels à l'œuvre
JVisualVM et Flight Recorder
JVisualVM affiche les threads actifs, leurs états et la mémoire; depuis Java 21, les threads virtuels sont affichés séparément. Java Flight Recorder (JFR) enregistre une « boîte noire » détaillée de l'exécution, y compris des statistiques sur Virtual Threads — pratique pour trouver les goulots d'étranglement.
Comment voir le nombre de threads dans le code
Une manière simple de voir le nombre de threads dans la JVM:
System.out.println("Threads au total: " + Thread.activeCount());
Compter combien d'entre eux sont virtuels:
long vCount = Thread.getAllStackTraces().keySet().stream()
.filter(Thread::isVirtual)
.count();
System.out.println("Threads virtuels: " + vCount);
6. Erreurs courantes lors de l'utilisation des threads virtuels
Erreur n°1: Utiliser des threads virtuels pour des calculs lourds. Lancer des millions de threads virtuels avec des tâches CPU‑bound n'accélérera pas le processeur. Virtual Threads n'est pas un « turbo » pour les calculs.
Erreur n°2: Reprise aveugle des anciens patterns. Ne créez pas de pools fixes de threads virtuels. Utilisez Executors.newVirtualThreadPerTaskExecutor() et laissez la JVM passer à l'échelle automatiquement.
Erreur n°3: Utiliser des bibliothèques non prises en charge. Les verrous natifs et les bibliothèques non adaptées à Loom peuvent provoquer des blocages et des baisses de performances. Vérifiez la compatibilité à l'avance.
Erreur n°4: Optimisation prématurée. Si vous n'avez que quelques threads et une multithread classique, ne vous précipitez pas pour tout migrer vers Virtual Threads. L'outil est pertinent là où il y a beaucoup d'I/O et d'attente.
Erreur n°5: Ignorer le monitoring. Créer un million de tâches est facile, mais sans monitoring et gestion des exceptions, vous risquez d'obtenir un « beau benchmark » plutôt qu'un système fiable. Utilisez JVisualVM et JFR.
GO TO FULL VERSION