Aller au contenu principal

Atelier 16 - Système de traitement de commandes

Créez un programme Java qui simule un système de traitement de commandes pour un entrepôt, en utilisant les streams parallèles et le multithreading.

Spécifications

1. Enum Categorie

Créez un enum Categorie avec les valeurs : ELECTRONIQUE, VETEMENT, ALIMENTAIRE, SPORT, MAISON.

2. Classe Produit

Créez une classe Produit avec les attributs suivants :

  • id (int)
  • nom (String)
  • categorie (Categorie)
  • prix (double)
  • stock (int) — quantité disponible en entrepôt

Redéfinissez toString() pour afficher :

[CATEGORIE] Nom — $prix (stock unités)

3. Catalogue de produits

Créez la liste initiale avec List.of() — au moins 15 produits couvrant toutes les catégories, avec des prix et stocks variés.

Cette liste est immuable. Pour les opérations qui modifient le stock (section 6), utilisez une copie modifiable : new ArrayList<>(...).


Section 4 — Streams parallèles

4a. Comparaison séquentiel vs parallèle

Générez une liste de 1 000 000 de nombres aléatoires entre 1 et 500 000 (new Random().longs(1_000_000, 1, 500_000).boxed().collect(...)), puis mesurez le temps d'exécution pour :

  1. Un stream() séquentiel qui calcule la somme des diviseurs de chaque nombre (utilisez une méthode sommeDiviseurs(long n))
  2. Un parallelStream() avec la même opération

Affichez les deux durées en millisecondes.

La méthode sommeDiviseurs doit additionner tous les entiers de 1 à n qui divisent n exactement. Par exemple, sommeDiviseurs(12) = 1 + 2 + 3 + 4 + 6 + 12 = 28.

C'est une opération CPU-intensive par élément — chaque appel fait jusqu'à n itérations — ce qui rend le jeu de données idéal pour observer le gain du parallélisme.

4b. Analyse du catalogue avec parallelStream()

À partir du catalogue de produits, utilisez parallelStream() pour calculer :

  1. La valeur totale du stock (prix * stock) pour tout l'entrepôt, avec mapToDouble et sum()
  2. Le produit le plus cher de chaque catégorie, avec Collectors.groupingBy et Collectors.maxBy
  3. La liste des produits en rupture imminente (stock < 5)
  4. Le prix moyen par catégorie, avec Collectors.groupingBy et Collectors.averagingDouble

4c. .parallel() sur un IntStream

Utilisez IntStream.range(0, catalogue.size()).parallel() pour afficher l'indice et le nom de chaque produit, précédé du nom du thread qui le traite (Thread.currentThread().getName()).

Observez que plusieurs threads différents apparaissent dans la sortie.

4d. Mauvaise utilisation — à corriger

Le code suivant contient un problème. Identifiez-le, corrigez-le, et expliquez pourquoi il est incorrect :

List<String> nomsCollectes = new ArrayList<>();

catalogue.parallelStream()
.map(Produit::getNom)
.forEach(nom -> nomsCollectes.add(nom)); // problème ici

System.out.println("Noms collectés : " + nomsCollectes.size());

Indice : ArrayList n'est pas thread-safe.

Section 5 — Création de threads

Créez trois threads qui analysent chacun une portion du catalogue. Chaque thread affiche le nombre de produits et la valeur totale du stock pour sa catégorie assignée.

Créez ces trois threads de trois façons différentes :

  1. Héritage de Thread : créez une classe AnalyseurCategorie extends Thread qui reçoit la catégorie et la liste en constructeur
  2. Implémentation de Runnable : créez une classe RapportCategorie implements Runnable avec la même logique
  3. Lambda : créez un troisième thread directement avec une expression lambda

Lancez les trois threads avec start().

Rappel : appelez start() et non run(). run() exécute la logique dans le thread courant — pas de concurrence.

Section 6 — join() et synchronisation du thread principal

6a. join()

Reprenez les trois threads de la section 5. Après les avoir lancés, appelez join() sur chacun.

Ajoutez une ligne après les join() :

=== Rapport d'analyse complet ===

Vérifiez que cette ligne s'affiche toujours après que les trois threads ont terminé.

Sans join(), le thread principal peut afficher "Rapport complet" avant que les analyses soient terminées.

6b. Race condition — commandes simultanées

Créez une classe Entrepot avec un attribut stock (int) initialisé à 100 et une méthode traiterCommande(int quantite) qui :

  1. Vérifie si stock >= quantite
  2. Si oui, soustrait la quantité du stock

Version sans synchronisation :

Lancez 10 threads qui appellent chacun entrepot.traiterCommande(15). Faites en sorte que le programme affiche le stock final.

Exécutez plusieurs fois — observez que le stock peut devenir négatif (race condition).

Version avec synchronized :

Ajoutez synchronized à la méthode traiterCommande. Relancez — le stock ne doit jamais descendre en dessous de 0.


Section 7 — Deadlock

7a. Mise en situation

Créez deux produits partageables, produitA et produitB, chacun avec un stock de 500 unités.

Créez deux threads qui simulent des transferts de stock :

  • Thread 1 : prend le verrou de produitA, attend 100 ms, puis prend le verrou de produitB pour transférer 50 unités de A vers B
  • Thread 2 : prend le verrou de produitB, attend 100 ms, puis prend le verrou de produitA pour transférer 50 unités de B vers A
// Thread 1
synchronized (produitA) {
Thread.sleep(100);
synchronized (produitB) {
produitA.stock -= 50;
produitB.stock += 50;
}
}

// Thread 2
synchronized (produitB) {
Thread.sleep(100);
synchronized (produitA) {
produitB.stock -= 50;
produitA.stock += 50;
}
}

Lancez les deux threads. Observez que le programme ne se termine jamais — c'est un deadlock.

7b. Solution

Corrigez le deadlock en faisant en sorte que les deux threads acquièrent les verrous dans le même ordre (toujours produitA d'abord, puis produitB).

Relancez — les deux threads doivent se terminer normalement.


Section 8 — Pipeline combiné

Écrivez un seul pipeline (sans variables intermédiaires) avec parallelStream() qui :

  1. Filtre les produits dont le stock est supérieur à 10
  2. Filtre les produits dont le prix est inférieur à 100.00$
  3. Trie par prix croissant
  4. Prend les 5 premiers
  5. Transforme en chaîne "Nom ($prix)"
  6. Collecte dans une List<String>

Affichez le résultat.


Exemple d'exécution attendu

=== Comparaison séquentiel vs parallèle ===
Séquentiel : 3842 ms
Parallèle : 987 ms

=== Valeur totale du stock ===
Valeur totale : $148 320.00

=== Produit le plus cher par catégorie ===
ELECTRONIQUE : [ELECTRONIQUE] MacBook Pro — $2499.99 (12 unités)
VETEMENT : [VETEMENT] Manteau hiver — $189.99 (34 unités)
...

=== Produits en rupture imminente (stock < 5) ===
[SPORT] Raquette de tennis — $89.99 (3 unités)
...

=== Threads d'analyse ===
ForkJoinPool.commonPool-worker-1 traite l'indice 0 : MacBook Pro
ForkJoinPool.commonPool-worker-3 traite l'indice 1 : iPhone 15
...

=== Analyse par catégorie ===
[Thread-0] ELECTRONIQUE — 4 produits, valeur stock : $32 450.00
[Thread-1] VETEMENT — 3 produits, valeur stock : $8 740.50
[Thread-2] SPORT — 3 produits, valeur stock : $4 210.00
=== Rapport d'analyse complet ===

=== Race condition (sans synchronized) ===
Stock final : -30 ← valeur incorrecte possible

=== Avec synchronized ===
Stock final : 10 ← toujours >= 0

=== Deadlock (version incorrecte) ===
Virement 1 : tient produitA, attend produitB...
Virement 2 : tient produitB, attend produitA...
[programme suspendu indéfiniment]

=== Deadlock corrigé ===
Transfert 1 terminé.
Transfert 2 terminé.

=== Top 5 produits abordables en stock ===
[Clé USB ($12.99), Chaussettes sport ($8.50), ...]

Contraintes

  • La liste initiale doit être créée avec List.of()
  • La section 4a doit utiliser une méthode sommeDiviseurs avec une boucle de 1 à n — pas d'appel à une librairie externe
  • La section 4b doit utiliser parallelStream() — pas stream()
  • La section 4d doit identifier le problème et fournir la version corrigée (avec Collectors.toList())
  • Les trois threads de la section 5 doivent être créés de trois façons distinctes
  • join() est obligatoire en section 6a
  • La section 6b doit montrer les deux versions : sans et avec synchronized
  • Le deadlock de la section 7a doit être démontrable — ne pas le corriger directement, le montrer puis le corriger en 7b
  • Le pipeline de la section 8 doit être écrit sans variables intermédiaires

Questions de réflexion

  • Pourquoi parallelStream() n'est-il pas toujours plus rapide qu'un stream() séquentiel ? Dans quel cas le parallélisme est-il contre-productif ?
  • Quelle est la différence entre concurrence et parallélisme ? Peut-on avoir de la concurrence sans plusieurs cœurs ?
  • Pourquoi start() et non run() pour lancer un thread ? Que se passe-t-il concrètement si on appelle run() directement ?
  • Qu'est-ce qu'une race condition ? Pourquoi est-elle difficile à reproduire de façon systématique en tests ?
  • Pourquoi synchronized dégrade-t-il la performance ? Comment minimiser cet impact ?
  • Quelle est la différence entre un deadlock et un livelock ? Donnez un exemple de situation réelle pour chacun.
  • Selon la loi d'Amdahl, si 20 % de votre programme est séquentiel, quel est le gain maximum théorique, peu importe le nombre de cœurs ?
  • Dans quel cas préféreriez-vous Runnable à l'héritage de Thread ? Et à l'inverse ?