1. Comment créer des threads virtuels en pratique
Passons de la théorie à la pratique ! Vous savez déjà comment on crée un thread de manière classique :
Thread t = new Thread(() -> System.out.println("Hello from thread!"));
t.start();
Ou un peu plus court :
new Thread(() -> System.out.println("Hi!")).start();
Avec Java 21, une nouvelle manière est apparue :
Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));
ou de façon plus explicite :
Thread t = Thread.ofVirtual().start(() -> System.out.println("Hello from virtual thread!"));
Quelle est la différence ?
- Thread.ofVirtual().start(...) crée un thread virtuel (Virtual Thread), géré par la JVM et non par l’OS.
- Thread.ofPlatform().start(...) (ou new Thread(...)) — un thread classique, comme auparavant.
Pourquoi est-ce important ?
On peut créer des dizaines de milliers de threads virtuels sans craindre OutOfMemoryError. Désormais, si vous décidez de traiter un million de requêtes — Java dira: « Pas de problème, encore ! »
2. Syntaxe de création d’un thread virtuel
Exemple de base :
public class VirtualThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.ofVirtual().start(() -> {
System.out.println("Bonjour depuis un thread virtuel ! Thread : " + Thread.currentThread());
});
// On attend la fin du thread (pour éviter que main ne se termine avant)
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Que se passe-t-il ?
- Nous créons un thread virtuel via Thread.ofVirtual().start(...).
- À l’intérieur du thread — une action simple : afficher un message.
- À la fin, nous appelons thread.join() pour que le thread principal attende la fin du thread virtuel (sinon le programme peut se terminer avant que le thread n’ait eu le temps d’afficher quelque chose).
À noter :
Un thread virtuel ressemble et se comporte presque comme un thread ordinaire, mais à l’intérieur — c’est la magie de la JVM !
3. Création massive de threads virtuels: la puissance de Loom en pratique
Essayons maintenant ce qui, avec des threads classiques, serait risqué (ou simplement impossible) : créons 10_000 threads virtuels, chacun affichant son numéro.
public class VirtualThreadMassive {
public static void main(String[] args) throws InterruptedException {
int N = 10_000;
Thread[] threads = new Thread[N];
for (int i = 0; i < N; i++) {
int threadNum = i;
threads[i] = Thread.ofVirtual().start(() -> {
System.out.println("Thread virtuel #" + threadNum + " en cours d'exécution !");
});
}
// On attend la fin de tous les threads
for (Thread t : threads) {
t.join();
}
System.out.println("Tous les threads virtuels sont terminés !");
}
}
- Pour des threads classiques (new Thread(...)), un tel code fera presque à coup sûr « planter » votre programme avec OutOfMemoryError.
- Pour des threads virtuels — c’est le mode normal ! La JVM gère facilement des milliers et des dizaines de milliers de threads.
D’ailleurs, si 10_000 vous semble beaucoup, essayez 100_000 ou même 1_000_000. Sur une machine moderne, la JVM s’en sortira si vos threads effectuent un travail simple ou attendent des E/S.
4. Runnable et expressions lambda: comment passer du code à un thread virtuel
Les threads virtuels acceptent les tâches comme les threads classiques : via l’interface Runnable. Cela signifie que vous pouvez passer une expression lambda, une référence de méthode, ou n’importe quel objet implémentant Runnable.
Exemple avec une lambda :
Thread.ofVirtual().start(() -> System.out.println("Lambda dans un thread virtuel !"));
Exemple avec une méthode :
public class TaskRunner {
public static void main(String[] args) {
Thread.ofVirtual().start(TaskRunner::doWork);
}
static void doWork() {
System.out.println("Nous travaillons dans un thread virtuel : " + Thread.currentThread());
}
}
Exemple avec une classe anonyme :
Thread.ofVirtual().start(new Runnable() {
@Override
public void run() {
System.out.println("Classe anonyme dans un thread virtuel !");
}
});
Conclusion :
Tout ce qui fonctionnait avec des threads classiques fonctionne également avec des threads virtuels — mais désormais c’est « léger et rapide ».
5. Comparaison avec ExecutorService: ancienne et nouvelle approche
ExecutorService classique
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("Tâche n°" + taskNum + " en cours d'exécution");
});
}
executor.shutdown();
Problème :
Si les tâches sont trop nombreuses et les threads trop peu — les tâches attendent dans la file. S’il y a trop de threads — le programme « s’étouffe » par manque de ressources.
Nouvelle approche: un Executor sur threads virtuels
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100_000; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("Tâche virtuelle n°" + taskNum);
});
}
executor.shutdown();
Que se passe-t-il ?
- Un thread virtuel distinct est créé pour chaque tâche.
- La JVM gère elle-même leur ordonnancement, sans surcharger le système.
- Pas besoin de limiter la taille du pool — les threads virtuels sont « presque gratuits ».
Quand utiliser un Executor à threads virtuels ?
- Quand vous avez un grand flux de tâches et que vous ne voulez pas penser à la taille du pool.
- Quand les tâches sont indépendantes et peuvent s’exécuter en parallèle.
- Quand vous souhaitez intégrer les threads virtuels dans une architecture existante utilisant déjà ExecutorService (par exemple, dans un serveur web, un moteur de traitement, etc.).
Conseil :
En cas de doute — commencez par un Executor à threads virtuels. C’est l’approche la plus universelle et moderne.
6. Conseils pratiques: quand utiliser quoi
Quand utiliser Thread.ofVirtual().start() directement ?
- Quand vous devez créer un thread séparé pour une tâche unique (par exemple, un test, une démo ou une simple expérimentation).
- Quand le nombre de threads est faible et que vous souhaitez les gérer manuellement.
Quand utiliser Executors.newVirtualThreadPerTaskExecutor() ?
- Quand vous devez lancer des tâches en masse (p. ex., traitement d’un grand nombre de requêtes, de fichiers, de connexions réseau).
- Quand les tâches sont indépendantes et ne nécessitent pas de coordination entre elles.
- Quand vous voulez intégrer les threads virtuels à une architecture existante qui utilise déjà ExecutorService (par exemple, dans un serveur web, un planificateur de tâches, etc.).
Conseil :
Si vous n’êtes pas sûr — commencez par un Executor à threads virtuels. C’est l’approche la plus polyvalente et la plus moderne.
7. Gestion des exceptions dans les threads virtuels
Les threads virtuels — ce sont des threads ordinaires du point de vue try-catch. Si une exception se produit à l’intérieur de votre Runnable, elle ne « casse » pas toute la JVM, mais termine simplement le thread concerné avec une erreur.
Exemple :
Thread t = Thread.ofVirtual().start(() -> {
throw new RuntimeException("Quelque chose s'est mal passé !");
});
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Le thread principal a continué à s'exécuter.");
Dans ExecutorService :
Si vous soumettez une tâche via submit, vous pouvez obtenir le résultat via un Future, et l’exception sera propagée lors de l’appel à get() :
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> f = executor.submit(() -> {
throw new RuntimeException("Erreur dans la tâche virtuelle");
});
try {
f.get();
} catch (ExecutionException e) {
System.out.println("Erreur capturée depuis le thread virtuel : " + e.getCause());
}
executor.shutdown();
8. Erreurs courantes lors de la création de threads virtuels
Erreur n°1: Confondre threads virtuels et threads de plateforme. Si vous créez des threads via new Thread(...) ou Thread.ofPlatform(), ce ne sont pas des threads virtuels. Seuls Thread.ofVirtual().start(...) ou les méthodes de Executors vous donnent de vrais Virtual Threads.
Erreur n°2: S’attendre à une accélération pour des calculs lourds. Les threads virtuels n’accélèrent pas les tâches fortement consommatrices de CPU (CPU-bound). Si vous avez un million de threads, chacun calculant Pi jusqu’au millionième chiffre — la JVM ne peut pas « accélérer » le calcul, elle ne fera que basculer entre les threads.
Erreur n°3: Allouer des ressources (par ex., bases de données) par thread. Si vous créez un million de threads virtuels mais que chacun nécessite une connexion séparée à la base — la base ne tiendra pas. Les threads virtuels conviennent aux tâches où l’essentiel du temps est de l’attente (E/S), et non l’utilisation de ressources externes limitées.
Erreur n°4: Ne pas attendre la fin des threads quand c’est important. Si le thread principal se termine avant les threads virtuels — le programme peut s’arrêter sans attendre les résultats. Utilisez join() ou ExecutorService avec shutdown() et awaitTermination().
Erreur n°5: Utiliser des bibliothèques obsolètes non compatibles avec les threads virtuels. Certaines bibliothèques tierces peuvent bloquer des threads au niveau de l’OS ou utiliser une synchronisation native, ce qui réduit l’efficacité des threads virtuels. Vérifiez toujours la compatibilité.
GO TO FULL VERSION