En Java, les pools de threads sont essentiels pour gérer l'exécution concurrente. Voici les six types les plus courants :
- FixedThreadPool
- CachedThreadPool
- ScheduledThreadPool
- SingleThreadExecutor
- SingleThreadScheduledExecutor
- ForkJoinPool
FixedThreadPool
Le FixedThreadPool possède un nombre fixe de threads. Les threads de base et le maximum sont identiques, donc une fois initialisés, le nombre de threads ne change pas. Si plus de tâches sont soumises que de threads, elles sont mises en file d'attente. Même si la file est pleine, aucun nouveau thread n'est créé. Exemple :
ExecutorService service = Executors.newFixedThreadPool(10);
CachedThreadPool
Le CachedThreadPool peut créer un nombre quasi illlimité de threads (jusqu'à Integer.MAX_VALUE). Il utilise une SynchronousQueue qui ne stocke pas les tâches mais les transmet directement. Si un thread est libre, la tâche lui est assignée ; sinon, un nouveau thread est créé. Les threads inactifs sont détruits après 60 secondes.
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
service.execute(() -> {
// tâche longue
});
}
ScheduledThreadPool
Il permet d'exécuter des tâches de manière différée ou périodique. Trois méthodes principales :
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
// Exécution unique après 10 secondes
service.schedule(() -> {}, 10, TimeUnit.SECONDS);
// Tâche périodique à taux fixe (délai entre débuts)
service.scheduleAtFixedRate(() -> {}, 10, 10, TimeUnit.SECONDS);
// Tâche périodique avec délai fixe entre la fin et le début suivant
service.scheduleWithFixedDelay(() -> {}, 10, 10, TimeUnit.SECONDS);
La différence : scheduleAtFixedRate démarre la prochaine exécution après un intervalle depuis le début de la précédente, tandis que scheduleWithFixedDelay attend que la tâche se termine avant de compter le délai.
SingleThreadExecutor
Utilise un seul thread pour exécuter toutes les tâches dans l'ordre de soumission. Si le thread meurt, il en recrée un nouveau. Idéal pour garantir l'ordre séquetniel.
ExecutorService service = Executors.newSingleThreadExecutor();
SingleThreadScheduledExecutor
Variente de ScheduledThreadPool avec un seul thread :
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
ForkJoinPool
Introduit en JDK 7, il est conçu pour les tâches pouvant être divisées en sous-tâches (Fork/Join). Contrairement aux autres pools, chaque thread possède sa propre file d'attente (deque) et peut voler du travail aux autres threads (work-stealing).
Exemple avec le calcul de Fibonacci :
class CalculFibonacci extends RecursiveTask<Integer> {
private int n;
public CalculFibonacci(int n) {
this.n = n;
}
@Override
protected Integer compute() {
if (n <= 1) return n;
CalculFibonacci f1 = new CalculFibonacci(n - 1);
f1.fork();
CalculFibonacci f2 = new CalculFibonacci(n - 2);
f2.fork();
return f1.join() + f2.join();
}
}
public static void main(String[] args) throws Exception {
ForkJoinPool pool = new ForkJoinPool();
for (int i = 0; i < 10; i++) {
int result = pool.submit(new CalculFibonacci(i)).get();
System.out.println(result);
}
}
Résultat pour les indices 0 à 9 : 0, 1, 1, 2, 3, 5, 8, 13, 21, 34.
Pourquoi éviter Executors.new* ?
Les pools créés via Executors présentent des risques :
- FixedThreadPool et SingleThreadExecutor : utilisent une
LinkedBlockingQueuesans limite de capacité, pouvant entraîner unOutOfMemoryErroren cas d'accumulation de tâches. - CachedThreadPool : nombre maximum de threads
Integer.MAX_VALUE, peut créer trop de threads et épuiser les ressources système. - ScheduledThreadPool et SingleThreadScheduledExecutor : utilisent
DelayedWorkQueue, une file non bornée, également source d'OOM.
Il est préférable de créer manuellement un ThreadPoolExecutor en spécifiant des limites explicites (taille de file bornée, politique de saturation) pour maîtriser l'utilisation des ressources.