Introduction : des synchronized de base aux outils modernes
Java est l'un des rares langages à offrir un support intégré du multithreading depuis sa création. Cependant, au niveau débutant, les développeurs se limitent souvent au mot-clé synchronized et aux simples Thread. Dans les systèmes réels à forte charge, cela ne suffit pas : les verrous génèrent des deadlocks, et la création naïve de threads entraîne une surcharge de la JVM.
Dans cet article, nous allons explorer les mécanismes avancés de concurrence en Java qui permettent d'écrire un code multithread efficace, scalable et sûr. Ce contenu s'adresse aux développeurs déjà familiers avec les bases du multithreading (moniteurs, wait/notify, volatile).
Nous aborderons quatre sujets clés : le Framework Fork/Join pour les tâches récursives, CompletableFuture pour la programmation asynchrone, StampedLock comme alternative à ReadWriteLock et l'utilisation judicieuse de ThreadLocal.
1. Framework Fork/Join : division récursive des tâches
Le Framework Fork/Join est apparu dans Java 7 et est conçu pour utiliser efficacement les processeurs multicœurs lors de la résolution de tâches pouvant être divisées en sous-tâches (diviser pour régner). La classe principale est ForkJoinPool, qui utilise le mécanisme de « vol de tâches » (work-stealing).
1.1. Principe de fonctionnement
Au lieu de créer des threads manuellement, vous décrivez une tâche comme héritant de RecursiveTask (si elle a une valeur de retour) ou de RecursiveAction (sans retour). Ensuite, vous divisez récursivement la tâche en parties jusqu'à ce qu'elles soient suffisamment petites pour être exécutées directement.
1.2. Exemple : somme parallèle d'un tableau
import java.util.concurrent.RecursiveTask;import java.util.concurrent.ForkJoinPool;
public class SumTask extends RecursiveTask<Long> { private static final int THRESHOLD = 10_000; private final int[] array; private final int start, end;
public SumTask(int[] array, int start, int end) { this.array = array; this.start = start; this.end = end; }
@Override protected Long compute() { if (end - start <= THRESHOLD) { long sum = 0; for (int i = start; i < end; i++) { sum += array[i]; } return sum; } int mid = start + (end - start) / 2; SumTask left = new SumTask(array, start, mid); SumTask right = new SumTask(array, mid, end); left.fork(); // lance la partie gauche de manière asynchrone long rightResult = right.compute(); // calcule la partie droite dans le thread actuel long leftResult = left.join(); // attend le résultat de la partie gauche return leftResult + rightResult; }
public static void main(String[] args) { int[] data = new int[1_000_000]; for (int i = 0; i < data.length; i++) data[i] = i;
ForkJoinPool pool = ForkJoinPool.commonPool(); long result = pool.invoke(new SumTask(data, 0, data.length)); System.out.println("Somme : " + result); }}Important : utilisez ForkJoinPool.commonPool() — il est déjà optimisé pour le nombre de cœurs. Évitez de créer vos propres pools sans nécessité.
2. CompletableFuture : asynchrone sans l'enfer des callbacks
Avant Java 8, la programmation asynchrone était possible via Future, mais il était synchrone (appel bloquant get()). CompletableFuture résout ce problème en fournissant une API fonctionnelle puissante pour la composition d'opérations asynchrones.
2.1. Scénarios de base
Vous pouvez créer des CompletableFuture, lancer des tâches de manière asynchrone et les chaîner :
import java.util.concurrent.CompletableFuture;
public cl