JEP 444 - Virtual Threads

Threads virtuels légers pour simplifier et améliorer la programmation concurrente en Java

Numéro JEP

444

Statut

Final

Introduit dans

Java 21 (LTS)

En résumé

Les threads virtuels sont des threads légers gérés par la JVM plutôt que par le système d'exploitation. Ils permettent d'écrire du code concurrent simple (style thread-per-request) tout en supportant des millions de threads simultanés. Révolution majeure : combinez la simplicité du code synchrone avec les performances de l'asynchrone, sans réécrire votre code.

Contexte et motivation

Le problème avec les threads traditionnels

Les threads Java traditionnels (appelés platform threads) sont coûteux car :

  • Chaque thread correspond à un thread OS (système d'exploitation)
  • Création et changement de contexte sont lents
  • Consommation mémoire importante (~1 Mo par thread)
  • Limite au nombre de threads (quelques milliers max)

Exemple typique : une application web qui traite des requêtes. Avec le modèle "un thread par requête", vous êtes limité à quelques milliers de requêtes simultanées.

Solutions existantes et leurs limites

Pour contourner ce problème, plusieurs approches ont été développées :

  • Programmation asynchrone (CompletableFuture, réactive) : performant mais complexe, code difficile à lire et débuguer
  • Thread pools : réutilise les threads mais ne résout pas le problème fondamental de leur coût

Solution : les threads virtuels

Les threads virtuels apportent le meilleur des deux mondes :

  • Simplicité : code synchrone classique, facile à lire et débuguer
  • Performance : des millions de threads sans problème
  • Compatibilité : API java.lang.Thread existante, migration facile
  • Pas de réécriture : votre code existant fonctionne avec les virtual threads

Comment ça marche ?

Les threads virtuels sont :

  • Gérés par la JVM (pas par l'OS)
  • Très légers (~1 Ko en mémoire)
  • Montés sur des platform threads (carrier threads) au besoin
  • Automatiquement "parkés" lors d'opérations bloquantes (I/O, sleep, etc.)

Quand un virtual thread fait une opération bloquante, la JVM le met en pause et libère le platform thread pour qu'un autre virtual thread puisse l'utiliser.

Exemples de code

Créer un virtual thread

// Avant (platform thread)
Thread thread = new Thread(() -> {
    System.out.println("Hello from platform thread");
});
thread.start();

// Avec virtual threads
Thread vThread = Thread.startVirtualThread(() -> {
    System.out.println("Hello from virtual thread");
});

Utiliser un ExecutorService avec virtual threads

// Créer un executor qui utilise des virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {

    // Soumettre 10 000 tâches (impossible avec platform threads !)
    for (int i = 0; i < 10_000; i++) {
        int taskId = i;
        executor.submit(() -> {
            // Simulation d'un appel réseau
            Thread.sleep(Duration.ofSeconds(1));
            return "Résultat de la tâche " + taskId;
        });
    }

} // Auto-fermeture et attente de toutes les tâches

Serveur web simple avec thread-per-request

void handleRequest(Socket socket) throws IOException {
    // Code synchrone simple et lisible
    var request = readRequest(socket);
    var user = fetchUser(request.userId());        // Appel DB
    var orders = fetchOrders(user.id());           // Appel DB
    var response = buildResponse(user, orders);
    writeResponse(socket, response);
}

// Serveur principal
try (var serverSocket = new ServerSocket(8080)) {
    while (true) {
        Socket socket = serverSocket.accept();

        // Un virtual thread par requête
        Thread.startVirtualThread(() -> handleRequest(socket));
    }
}

💡 Remarque : Ce code peut gérer des dizaines/centaines de milliers de connexions simultanées, tout en restant simple à comprendre. Avec des platform threads, cela serait impossible.

Utilisation avec Thread.ofVirtual()

// Builder pour plus de contrôle
Thread vThread = Thread.ofVirtual()
    .name("mon-virtual-thread")
    .start(() -> {
        System.out.println("Exécution du virtual thread : " + Thread.currentThread());
    });

// Attendre la fin
vThread.join();

Exemple complet : traitement parallèle

import java.time.Duration;
import java.util.concurrent.*;
import java.util.stream.IntStream;

public class VirtualThreadsDemo {

    public static void main(String[] args) throws InterruptedException {
        // Mesurer le temps avec platform threads
        long startPlatform = System.currentTimeMillis();
        testWithPlatformThreads();
        long timePlatform = System.currentTimeMillis() - startPlatform;
        System.out.println("Platform threads: " + timePlatform + " ms");

        // Mesurer le temps avec virtual threads
        long startVirtual = System.currentTimeMillis();
        testWithVirtualThreads();
        long timeVirtual = System.currentTimeMillis() - startVirtual;
        System.out.println("Virtual threads: " + timeVirtual + " ms");
    }

    static void testWithPlatformThreads() throws InterruptedException {
        try (var executor = Executors.newFixedThreadPool(100)) {
            IntStream.range(0, 10_000).forEach(i -> {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofMillis(100));
                    return i;
                });
            });
        }
    }

    static void testWithVirtualThreads() throws InterruptedException {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, 10_000).forEach(i -> {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofMillis(100));
                    return i;
                });
            });
        }
    }
}

Bonnes pratiques

✅ À faire

  • Utiliser des virtual threads pour les tâches I/O-bound (réseau, DB, fichiers)
  • Créer des millions de virtual threads sans hésitation
  • Écrire du code synchrone simple et lisible
  • Utiliser Executors.newVirtualThreadPerTaskExecutor()

❌ À éviter

  • Ne pas utiliser de thread pools avec virtual threads (pas nécessaire !)
  • Éviter les opérations CPU-intensives dans des virtual threads (préférer platform threads)
  • Ne pas utiliser synchronized sur des sections longues (préférer ReentrantLock)
  • Éviter ThreadLocal si possible (coût mémoire multiplié par le nombre de virtual threads)

Impact et bénéfices

Pour le développeur

  • Code plus simple et plus lisible
  • Debugging et stack traces normales
  • Pas besoin d'apprendre la programmation réactive
  • Migration progressive depuis le code existant

Pour l'application

  • Meilleure utilisation des ressources
  • Latence réduite
  • Scalabilité améliorée (plus de connexions simultanées)
  • Coût infrastructure potentiellement réduit

Compatibilité et migration

Les virtual threads sont compatibles avec l'API Thread existante :

  • Tous les outils de monitoring et profiling fonctionnent
  • Les debuggers supportent les virtual threads
  • Les librairies existantes fonctionnent (JDBC, HttpClient, etc.)
  • Migration progressive possible (mélanger platform et virtual threads)

🎯 Migration recommandée : Commencez par remplacer vos ExecutorService par newVirtualThreadPerTaskExecutor() dans les parties I/O de votre application.

Ressources supplémentaires