CodeGym /Cours /JAVA 25 SELF /Flux parallèles : syntaxe et utilisation

Flux parallèles : syntaxe et utilisation

JAVA 25 SELF
Niveau 54, Leçon 2
Disponible

1. Rappel de l’API Stream

Vous connaissez déjà la Stream API — c’est un moyen pratique de travailler avec les collections, qui permet d’écrire un code compact et lisible pour le traitement des données : filtrage, tri, comptage, etc.

Voici un exemple classique :

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

int sum = numbers.stream()
    .filter(n -> n % 2 == 0)
    .mapToInt(n -> n)
    .sum();

System.out.println(sum); // 6 (2 + 4)

Dans cet exemple, la collection est transformée en flux (stream()), seuls les nombres pairs y sont retenus, ils sont ensuite convertis en int, et le résultat est sommé via l’appel à sum().

La Stream API rend le code plus court et plus expressif : au lieu de décrire étape par étape comment tout se passe, vous indiquez simplement ce que vous voulez obtenir. De plus, si nécessaire, on peut facilement basculer vers un traitement parallèle — en une seule ligne.

2. Flux parallèles : syntaxe et principe de fonctionnement

Comment rendre un flux parallèle ?

C’est simple : au lieu de stream(), utilisez parallelStream(). Ou appelez la méthode .parallel() sur un flux existant.

List<Integer> numbers = ...;

int sum = numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .mapToInt(n -> n)
    .sum();

Ou ainsi :

numbers.stream()
    .parallel() // on le transforme en flux parallèle
    .filter(...)
    .map(...)
    .sum();

Que se passe‑t‑il sous le capot ?

  • La collection est automatiquement découpée en parties.
  • Chaque partie est traitée dans un thread distinct (via ForkJoinPool — un pool de threads spécialisé).
  • Les résultats sont fusionnés en une valeur finale.

Autrement dit, si vous avez un processeur multicœur, le traitement se fait réellement en parallèle — par exemple, le filtrage et le calcul de la somme peuvent s’exécuter simultanément sur plusieurs cœurs.

Dans quels cas est-ce particulièrement utile ?

  • Traitement de grandes collections (des dizaines de milliers d’éléments et plus).
  • Calculs complexes pour chaque élément.
  • Absence de nécessité de conserver un ordre strict de traitement.

Exemple : comparaison entre flux séquentiel et flux parallèle

Regardons un exemple simple avec le traitement d’un grand tableau.

import java.util.*;
import java.util.stream.*;

public class ParallelStreamDemo {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.rangeClosed(1, 10_000_000)
                                         .boxed()
                                         .collect(Collectors.toList());

        // Séquentiel
        long time1 = System.currentTimeMillis();
        long count1 = numbers.stream()
            .filter(n -> n % 2 == 0)
            .count();
        long time2 = System.currentTimeMillis();
        System.out.println("Séquentiel : " + (time2 - time1) + " ms, nombres pairs : " + count1);

        // Parallèle
        long time3 = System.currentTimeMillis();
        long count2 = numbers.parallelStream()
            .filter(n -> n % 2 == 0)
            .count();
        long time4 = System.currentTimeMillis();
        System.out.println("Parallèle : " + (time4 - time3) + " ms, nombres pairs : " + count2);
    }
}

Essayez ce code sur votre ordinateur — il est fort probable que le flux parallèle traite la collection plus vite (surtout si vous avez un processeur multicœur). Mais pas toujours ! Les nuances — ci‑dessous.

3. Comment cela fonctionne : ForkJoinPool et découpage automatique

Les flux parallèles utilisent sous le capot ForkJoinPool.commonPool(), qui gère automatiquement le nombre de threads (généralement — selon le nombre de cœurs processeur disponibles).

Schématiquement :

+-----------------------------+
|        Votre collection     |
+-----------------------------+
| 1  | 2  | 3  | ... | 10 M   |
+----+----+----+-----+--------+
   |    |    |           |
   v    v    v           v
[Thread1][Thread2]...[ThreadN]
   |    |    |           |
   +----+----+-----------+
        |
   [Fusion des résultats]

Chaque thread traite sa partie, puis les résultats sont agrégés.

4. Limites et écueils

Les flux parallèles ne sont pas un bouton magique « tout accélérer ». Parfois, ils ralentissent même l’exécution !

Quand le parallélisme n’est pas avantageux :

  • La collection est petite (jusqu’à ~1000 éléments).
  • L’opération sur chaque élément est très rapide (par exemple simplement n * 2).
  • Vous avez besoin d’un ordre strict de traitement (par exemple pour une écriture séquentielle dans un fichier).

Pourquoi ? La création et la synchronisation des threads prennent aussi du temps. Si la tâche elle‑même est « petite », les surcoûts peuvent dépasser le gain du parallélisme.

Les effets de bord sont l’ennemi du parallélisme

Si vos opérations dans le flux modifient des variables externes, soyez prudents !

Mauvais exemple :

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int[] sum = {0};

numbers.parallelStream().forEach(n -> sum[0] += n);

System.out.println(sum[0]); // ??? (vous attendez 15, mais vous pouvez obtenir n’importe quoi)

Pourquoi ? Parce que plusieurs threads modifient simultanément la même variable — il se produit une race condition (état de concurrence). La valeur finale peut être incorrecte.

La bonne approche consiste à utiliser les méthodes du flux qui renvoient un résultat :

int sum = numbers.parallelStream().mapToInt(n -> n).sum();

Toutes les collections ne se parallélisent pas aussi bien

Certaines collections (par exemple un ArrayList classique) se découpent bien. En revanche, LinkedList ou un flux infini (par exemple Stream.generate(...)) — beaucoup moins.

5. Pratique : comparaison des performances

Exemple : recherche du nombre maximal

import java.util.*;
import java.util.stream.*;

public class ParallelMaxDemo {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.rangeClosed(1, 30_000_000)
                                         .boxed()
                                         .collect(Collectors.toList());

        // Séquentiel
        long t1 = System.currentTimeMillis();
        int max1 = numbers.stream().max(Integer::compareTo).get();
        long t2 = System.currentTimeMillis();
        System.out.println("Séquentiel : " + (t2 - t1) + " ms, max = " + max1);

        // Parallèle
        long t3 = System.currentTimeMillis();
        int max2 = numbers.parallelStream().max(Integer::compareTo).get();
        long t4 = System.currentTimeMillis();
        System.out.println("Parallèle : " + (t4 - t3) + " ms, max = " + max2);
    }
}

Que va‑t‑on observer ? Sur des processeurs multicœurs modernes, le flux parallèle est généralement plus rapide. Mais si vous remplacez 30_000_000 par 1000, la différence disparaît — et parfois le parallèle est même plus lent !

6. Exemples d’utilisation : filtrage, agrégation, tri

Filtrage et comptage

List<String> names = Arrays.asList("Anna", "Boris", "Basil", "Gregory", "Dіana", "George", "Eugene");

long count = names.parallelStream()
    .filter(name -> name.length() == 4)
    .count();

System.out.println("Noms de longueur 4 : " + count);

Regroupement

List<String> words = Arrays.asList("cat", "whale", "cat", "dog", "whale", "cat");

Map<String, Long> freq = words.parallelStream()
    .collect(Collectors.groupingBy(
        w -> w,
        Collectors.counting()
    ));

System.out.println(freq); // {dog=1, whale=2, cat=3}

Tri (mais ici le parallélisme n’apporte pas toujours un gain !)

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

long t1 = System.currentTimeMillis();
List<Integer> sorted = bigList.parallelStream()
    .sorted()
    .collect(Collectors.toList());
long t2 = System.currentTimeMillis();

System.out.println("Tri parallèle : " + (t2 - t1) + " ms");

7. Points importants et recommandations

Quand utiliser parallelStream()

  • La collection est grande (des dizaines de milliers d’éléments et plus).
  • L’opération sur l’élément est « lourde » (calculs complexes, travail avec fichiers/réseau).
  • Pas de dépendance à l’ordre des éléments.
  • Pas d’effets de bord (les variables externes ne sont pas modifiées).

Quand il NE faut PAS utiliser parallelStream()

  • La collection est petite.
  • L’opération est rapide.
  • Il faut conserver strictement l’ordre.
  • Il y a un accès à des variables partagées (envisagez des collections thread-safe ou d’autres approches).

Comment savoir combien de threads sont utilisés ?

Par défaut — selon le nombre de cœurs du processeur : Runtime.getRuntime().availableProcessors(). Vous pouvez modifier ce comportement via une propriété système :

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8");

Ne le faites que si vous en comprenez les conséquences — sinon vous risquez de saturer le processeur et d’obtenir des ralentissements.

8. Erreurs typiques avec les flux parallèles

Erreur n° 1 : Effets de bord à l’intérieur de forEach
Beaucoup pensent : « Maintenant je vais remplir une liste en parallèle ! »

List<Integer> result = new ArrayList<>();
IntStream.range(0, 1_000)
    .parallel()
    .forEach(result::add); // DANGEREUX !
System.out.println(result.size()); // Le résultat est aléatoire !

Pourquoi est‑ce mauvais ? ArrayList n’est pas thread-safe, et lors d’ajouts simultanés depuis plusieurs threads, le résultat est imprévisible : oublis, doublons, exceptions.

Solution : Utilisez les méthodes de collecte du flux (collect), qui garantissent elles‑mêmes la sécurité, ou des collections spécialisées.

List<Integer> result = IntStream.range(0, 1_000)
    .parallel()
    .boxed()
    .collect(Collectors.toList());

Erreur n° 2 : Attendre une accélération sur de petites tâches
Le parallélisme n’est pas gratuit ! Si la collection est petite, le flux parallèle peut fonctionner plus lentement à cause des surcoûts de planification et de synchronisation.

Erreur n° 3 : Violation de l’ordre
Si l’ordre des éléments est important (par exemple lors de l’écriture dans un fichier), n’utilisez pas les flux parallèles — l’ordre n’est pas garanti (ou ce sera plus lent).

Erreur n° 4 : Utiliser des collections « mal adaptées »
Certaines collections (par exemple LinkedList, structures non standard) se découpent mal — l’efficacité du parallélisme diminue.

Erreur n° 5 : Ignorer la sûreté vis‑à‑vis des threads lors de la collecte des résultats
Si vous collectez les résultats manuellement (par exemple en ajoutant dans une liste), utilisez des collections thread-safe (CopyOnWriteArrayList, ConcurrentLinkedQueue) ou les méthodes de collecte du flux.

1
Mission
JAVA 25 SELF, niveau 54, leçon 2
Bloqué
Mesure du potentiel énergétique des artefacts ✨
Mesure du potentiel énergétique des artefacts ✨
1
Mission
JAVA 25 SELF, niveau 54, leçon 2
Bloqué
Organisation du défilé des animaux 🐾
Organisation du défilé des animaux 🐾
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION