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 :
- Un
stream()séquentiel qui calcule la somme des diviseurs de chaque nombre (utilisez une méthodesommeDiviseurs(long n)) - 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'à
nité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 :
- La valeur totale du stock (
prix * stock) pour tout l'entrepôt, avecmapToDoubleetsum() - Le produit le plus cher de chaque catégorie, avec
Collectors.groupingByetCollectors.maxBy - La liste des produits en rupture imminente (stock < 5)
- Le prix moyen par catégorie, avec
Collectors.groupingByetCollectors.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 :
ArrayListn'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 :
- Héritage de
Thread: créez une classeAnalyseurCategorie extends Threadqui reçoit la catégorie et la liste en constructeur - Implémentation de
Runnable: créez une classeRapportCategorie implements Runnableavec la même logique - Lambda : créez un troisième thread directement avec une expression lambda
Lancez les trois threads avec start().
Rappel : appelez
start()et nonrun().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 :
- Vérifie si
stock >= quantite - 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 deproduitBpour transférer 50 unités de A vers B - Thread 2 : prend le verrou de
produitB, attend 100 ms, puis prend le verrou deproduitApour 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 :
- Filtre les produits dont le stock est supérieur à 10
- Filtre les produits dont le prix est inférieur à 100.00$
- Trie par prix croissant
- Prend les 5 premiers
- Transforme en chaîne
"Nom ($prix)" - 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
sommeDiviseursavec une boucle de 1 àn— pas d'appel à une librairie externe - La section 4b doit utiliser
parallelStream()— passtream() - 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'unstream()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 nonrun()pour lancer un thread ? Que se passe-t-il concrètement si on appellerun()directement ? - Qu'est-ce qu'une race condition ? Pourquoi est-elle difficile à reproduire de façon systématique en tests ?
- Pourquoi
synchronizeddé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 deThread? Et à l'inverse ?