CodeGym /Cours /JAVA 25 SELF /Bonnes pratiques de programmation parallèle

Bonnes pratiques de programmation parallèle

JAVA 25 SELF
Niveau 54 , Leçon 4
Disponible

1. Tout ce qui peut être parallélisé ne doit pas forcément l’être

En Java, il existe de nombreuses façons de paralléliser des tâches. Mais « parallélisme = toujours plus rapide » — c’est comme penser que si l’on ajoute encore du sel à une soupe, elle sera meilleure : jusqu’à un certain point — oui, mais au-delà — mieux vaut s’abstenir.

ExecutorService convient parfaitement lorsque vous avez des tâches explicites à lancer et à contrôler : traitement de requêtes, chargements asynchrones de données, calculs indépendants. Vous choisissez vous‑même le nombre de threads dans le pool et gérez le cycle de vie des tâches.

parallelStream — un moyen rapide de paralléliser le traitement de collections lorsque les opérations sont indépendantes et sans effets de bord. Rentable pour les collections « lourdes » (des dizaines de milliers d’éléments et plus).

ForkJoinPool — le choix pour les tâches qui se divisent bien en sous‑tâches (divide & conquer) : tri, recherche, agrégation de grands tableaux. Utilisé à l’intérieur de parallelStream, mais on peut aussi le piloter directement.

N’utilisez pas le parallélisme « au cas où ». Si la tâche est petite, les surcoûts de planification, de changement de contexte et de synchronisation peuvent absorber tout le gain.

Exemple : quand le parallélisme n’est pas nécessaire

List<Integer> smallList = List.of(1, 2, 3, 4, 5);
int sum = smallList.parallelStream()
    .mapToInt(x -> x)
    .sum(); // Paralléliser pour 5 nombres — overkill !

2. Thread-safety : éviter les données partagées modifiables

Dans le monde parallèle, la principale menace — les conditions de course (race conditions). Si plusieurs threads modifient une même variable, le résultat peut être inattendu.

  • Évitez les variables partagées modifiables. Même une expression comme counter++ n’est pas atomique.
  • Utilisez des collections thread-safe et des opérations atomiques. Par exemple, ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger, AtomicLong.
  • Pas d’effets de bord dans les streams parallèles. Ne modifiez pas des structures externes depuis parallelStream.

Mauvais exemple de code

List<Integer> numbers = Arrays.asList(1,2,3,4,5);
List<Integer> result = new ArrayList<>();
numbers.parallelStream().forEach(n -> result.add(n * 2)); // DANGEREUX !

Ici, result.add() n’est pas thread-safe. Résultat : éléments perdus ou exceptions.

Comment faire correctement ?

List<Integer> result = numbers.parallelStream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

3. Performance : plus de threads n’est pas toujours mieux

Les petites tâches ne sont pas rentables à paralléliser. Si le travail prend des millisecondes, un lancement parallèle ralentira souvent l’exécution à cause des surcoûts.

Mesurez la performance. Pour des mesures rapides, System.nanoTime() convient :

long start = System.nanoTime();
// ... votre code ...
long end = System.nanoTime();
System.out.println("Temps d'exécution: " + (end - start) + " ns");

Pour de vrais microbenchmarks, utilisez JMH (Java Microbenchmark Harness).

Exemple : comparaison d’un stream séquentiel et d’un stream parallèle

List<Integer> bigList = IntStream.range(0, 1_000_000)
    .boxed().collect(Collectors.toList());

long t1 = System.nanoTime();
long sum1 = bigList.stream().mapToLong(x -> x).sum();
long t2 = System.nanoTime();
long sum2 = bigList.parallelStream().mapToLong(x -> x).sum();
long t3 = System.nanoTime();

System.out.println("Séquentiel: " + (t2 - t1) / 1_000_000 + " ms");
System.out.println("Parallèle:     " + (t3 - t2) / 1_000_000 + " ms");

Essayez sur votre machine — le gain est perceptible sur des collections vraiment grandes et des opérations lourdes.

4. Gestion des erreurs : n’ignorez pas les exceptions dans les threads

Future et gestion des exceptions

Si vous lancez une tâche via ExecutorService.submit(), les exceptions ne « remontent » pas automatiquement — vous devez les gérer via Future.get() :

Future<Integer> future = executor.submit(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Oups !");
    return 42;
});
try {
    Integer result = future.get(); // peut lancer ExecutionException
} catch (ExecutionException e) {
    System.err.println("Erreur dans la tâche : " + e.getCause());
}

ForkJoin et gestion des erreurs

Dans ForkJoinPool, les exceptions sont encapsulées dans la tâche. Lors de l’appel de join()/get(), elles remonteront :

ForkJoinPool pool = new ForkJoinPool();
RecursiveTask<Integer> task = new MyTask();
try {
    int result = pool.invoke(task);
} catch (Exception e) {
    System.err.println("Erreur dans ForkJoin : " + e);
}

N’oubliez pas de gérer InterruptedException

De nombreuses méthodes (par exemple, Future.get(), Thread.sleep()) peuvent lancer InterruptedException. Ne l’« avalez » pas — réagissez correctement : réglez le drapeau d’interruption ou terminez la tâche.

5. Débogage et tests du code parallèle

Journalisation et débogage

Les bugs parallèles sont sournois et se manifestent souvent de manière instable. Journalisez en indiquant le thread : Thread.currentThread().getName(). Cela aide à comprendre qui exécute le code et quand.

Dans les cas complexes, utilisez un débogueur avec prise en charge du multithreading (par exemple, IntelliJ IDEA). Des Thread.sleep() temporaires aident parfois à « attraper » une course de données rare.

Tests de scénarios multithread

Dédiez des tests séparés aux opérations parallèles et utilisez des utilitaires d’attente de conditions, par exemple Awaitility. Exécutez ces tests de nombreuses fois : certains problèmes n’apparaissent qu’au 100e ou au 1000e lancement.

6. Nuances et conseils utiles

Lisibilité et maintenabilité : écrivez un code parallèle clair

  • Documentez. Commentez les sections complexes et le choix des outils.
  • Utilisez des abstractions de haut niveau. Préférez ExecutorService, parallelStream, ForkJoinPool au pilotage manuel des threads.
  • Évitez la « magie ». Ne compliquez pas la synchronisation s’il est possible de faire plus simple.

Tableau : quand utiliser quel outil

Scénario Outil recommandé
Beaucoup de tâches indépendantes
ExecutorService
Traitement d’une grande collection parallelStream ou ForkJoin
Tâche « divide & conquer »
ForkJoinPool + RecursiveTask
Tâche asynchrone simple
CompletableFuture
Beaucoup de petites tâches Stream séquentiel
Tâches avec effets de bord Uniquement des collections thread-safe !

« Commandements » du programmeur parallèle

  • N’utilisez pas de variables partagées modifiables — sauf si vous êtes sûr qu’elles sont thread-safe.
  • Ne parallélisez pas pour le plaisir de paralléliser : évaluez le gain potentiel.
  • N’oubliez pas de fermer les pools : shutdown()/shutdownNow().
  • N’utilisez pas parallelStream pour des opérations avec effets de bord.
  • N’oubliez pas de gérer les exceptions de Future et de ForkJoinTask.
  • N’« avalez » pas InterruptedException — terminez la tâche correctement.

7. Erreurs typiques en programmation parallèle

Erreur n° 1 : Parallélisation de petites tâches. Les débutants parallélisent souvent tout, même lorsque le travail prend des microsecondes. Résultat — plus lent à cause des surcoûts.

Erreur n° 2 : Effets de bord dans les streams. Dans parallelStream, il est interdit de modifier des variables ou collections externes — vous aurez des conditions de course et des bugs imprévisibles.

Erreur n° 3 : Ignorer les exceptions. Si vous ne gérez pas les erreurs de Future.get() ou ForkJoinTask, vous ne saurez pas pourquoi une tâche a échoué.

Erreur n° 4 : Oublier shutdown() sur ExecutorService. Sans arrêt explicite, l’application peut « se bloquer » à la sortie.

Erreur n° 5 : Utiliser des collections non thread-safe. Écrire depuis plusieurs threads dans un ArrayList ordinaire — voie directe vers les erreurs.

Erreur n° 6 : « Avaler » InterruptedException. Si un thread est interrompu — respectez‑le et terminez correctement le travail.

Erreur n° 7 : Logique de synchronisation trop complexe. Des blocs synchronized excessifs mènent à deadlock/livelock. Préférez les abstractions de haut niveau.

1
Étude/Quiz
Parallélisme et ForkJoin, niveau 54, leçon 4
Indisponible
Parallélisme et ForkJoin
Parallélisme et ForkJoin
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION