← Java SE 21 — préparation OCP

Module 6 / 8 · 90 min

Concurrence, exécuteurs et virtual threads

Distinguer visibilité, atomicité et capacité de traitement.

Lessons, exercises and questions in this path are in French. The interface follows the site language.

Your progress will appear here when JavaScript is active.

Progress and best score are stored only in this browser. No account is needed; clearing browser data removes them. Module times are estimates including exercises.

Comprendre le mécanisme

Des tâches concurrentes peuvent partager un état et entrelacer leurs opérations. volatile apporte des garanties de visibilité et d’ordre, pas l’atomicité d’une séquence comme count++. Utilisez verrou ou type atomique pour un invariant adapté. Un ExecutorService gère le lancement de tâches ; Future porte le résultat ou l’échec qu’il faut effectivement observer. Des collections concurrentes ne rendent pas automatiquement atomique une séquence de plusieurs appels.

Raisonner et vérifier

Les virtual threads de Java 21 aident à servir de nombreuses tâches principalement bloquantes. Ils ne multiplient pas le nombre de cœurs ni n’accélèrent magiquement un calcul CPU. Limitez l’accès aux ressources rares avec un mécanisme explicite. Dans Java 21, un bloc synchronized contenant une attente bloquante peut aussi être pertinent pour analyser le pinning ; raisonnez toujours avec la version ciblée.

Visibilité et atomicité ne se confondent pas

Un champ volatile apporte des garanties de visibilité et d’ordre définies par le modèle mémoire. Il ne transforme pas toute séquence en opération atomique. Un incrément comme count++ comporte une lecture, un calcul et une écriture ; deux threads peuvent lire le même ancien nombre. Définissez la propriété à protéger, puis choisissez une primitive atomique ou un verrou adapté, au lieu de supposer que la visibilité suffit.

Understand this common mistake

volatile rend automatiquement compteur++ atomique.

Lecture, calcul et écriture peuvent rester des étapes distinctes.

Protéger un invariant composé

Si un traitement doit vérifier un solde puis le modifier avec une autre donnée, protéger séparément chaque lecture ne rend pas automatiquement la transition entière cohérente. La zone de synchronisation doit correspondre à l’invariant partagé. Dans un service singleton, évitez aussi de conserver le marchand courant dans un champ mutable réutilisé par toutes les requêtes. Des paramètres locaux et immuables limitent ce partage involontaire de contexte.

Understand this common mistake

Un champ volatile suffit toujours à plusieurs mises à jour liées.

L’atomicité du groupe demande une stratégie adaptée.

Observer les résultats des tâches

Un Future peut représenter un résultat encore attendu, réussi ou en échec selon son cycle. Recevoir cet objet ne prouve pas que le travail métier a abouti. Le consommateur doit observer les résultats et les erreurs, puis appliquer une politique de reprise ou de refus. Abandonner silencieusement les références peut laisser des échecs invisibles tandis que l’appelant annonce déjà un succès.

Understand this common mistake

Obtenir un Future prouve déjà le succès du travail.

Le résultat peut être en attente ou en échec.

Gérer la durée des exécuteurs

Un exécuteur possède des ressources et un cycle de fermeture. En créer sans politique peut multiplier threads, files et travail restant. Définissez qui le possède et quand il cesse d’accepter des tâches. Un test doit couvrir la fin normale, une erreur et l’arrêt avec travail en attente. Le fait qu’un processus termine rapidement dans l’IDE ne prouve pas que toutes les ressources sont libérées proprement.

Understand this common mistake

Créer des pools indéfiniment ne consomme aucune ressource.

Threads et files demandent une gestion de durée de vie.

Annuler n’efface pas tout effet

Une demande d’annulation ne garantit pas un arrêt immédiat de chaque calcul ou de toute opération distante déjà validée. Le code doit coopérer selon le mécanisme utilisé et les dépendances peuvent continuer leur travail. Pour une mutation sensible, conservez une référence et un moyen de réconciliation. Un timeout ou une annulation côté client ne démontre pas que le service externe a laissé ses données intactes.

Understand this common mistake

cancel force toujours la fin immédiate de tout effet distant.

Le traitement et les dépendances peuvent continuer.

Traiter le signal d’interruption

L’interruption signale une demande à laquelle le code doit répondre selon son contrat. La capturer puis l’ignorer systématiquement peut empêcher un arrêt cohérent. Examinez la responsabilité du niveau courant et la façon de conserver ou propager l’information utile. Les tests doivent provoquer l’interruption pendant une attente contrôlée et vérifier que le traitement libère les ressources prévues sans annoncer un succès qui n’a pas eu lieu.

Understand this common mistake

Une interruption doit toujours être silencieusement ignorée.

Le code doit préserver une politique d’arrêt cohérente.

Threads virtuels et ressources réelles

Les threads virtuels Java 21 facilitent notamment de nombreuses tâches comportant des attentes bloquantes indépendantes. Ils ne créent pas de puissance CPU ni une capacité infinie chez un fournisseur. Pour limiter cinq opérations simultanées vers une ressource, un Semaphore peut représenter cette borne avec libération garantie. Définissez aussi la file et le délai : multiplier les tâches en attente ne retire pas le besoin de budgets.

Understand this common mistake

Un thread virtuel multiplie automatiquement la puissance CPU.

Ils ne créent pas de cœurs supplémentaires.

Opérations concurrentes et workflow composé

Une collection concurrente protège des opérations selon son contrat, pas nécessairement toute combinaison de plusieurs appels. Lire une valeur puis décider une écriture peut encore former une course. Cherchez une opération composée adaptée ou une synchronisation portant sur l’invariant entier. Un test utile interpose deux acteurs entre la lecture et l’écriture, au lieu de vérifier seulement plusieurs appels exécutés successivement.

Understand this common mistake

Deux opérations séparées deviennent atomiques car la Map est concurrente.

Un invariant composé peut demander une primitive spécifique.

Borner la file et la pression

Une file sans limite peut accumuler plus de travail que le service ne peut traiter dans le délai utile. La mémoire et la latence deviennent alors des ressources consommées avant même l’exécution. Choisissez capacité, refus et reprise selon le produit. Une limitation de concurrence sans limitation d’attente peut seulement déplacer le problème ; mesurez ensemble tâches actives, tâches en file et durée totale du parcours.

Understand this common mistake

Une file infinie est une garantie universelle de disponibilité.

L’accumulation peut épuiser la mémoire et la latence.

Tester les entrelacements pertinents

Un succès séquentiel ne démontre pas l’absence d’une course. Identifiez l’endroit où deux acteurs peuvent observer ou modifier le même état, puis contrôlez cette interposition avec des mécanismes de test appropriés. Évitez de dépendre uniquement de pauses aléatoires. Vérifiez l’invariant final, les erreurs et les effets externes ; augmenter seulement le nombre d’itérations ne remplace pas un scénario qui cible réellement la frontière.

Understand this common mistake

Un succès séquentiel prouve l’absence de tout interleaving dangereux.

Les exécutions concurrentes peuvent produire d’autres ordres.

Atelier guidé : lire, expliquer et vérifier le code

class Counter {
    volatile int value;
    void increment() { value++; }
}

Le champ volatile du compteur ne suffit pas à rendre l’incrément composé indivisible. Deux threads peuvent lire une même valeur, calculer chacun la suivante et écrire le même résultat, perdant ainsi une augmentation. Il ne s’agit pas d’une erreur de syntaxe ni d’une conversion implicite vers AtomicInteger : le champ reste un int et les étapes du calcul restent à analyser.

Dans un laboratoire, comparez une primitive d’incrément atomique avec un invariant portant sur plusieurs champs. La première peut convenir au simple compteur, alors que le second demande une protection de groupe. Ajoutez un scénario de service singleton avec un contexte marchand passé en paramètre. Vérifiez que deux appels concurrents ne se contaminent pas, même lorsque leurs étapes sont volontairement entrelacées.

Singleton Spring : état de requête et concurrence

@Service
public class PaymentService {
    private String merchantId;
    public void pay(String id) {
        merchantId = id;
        // ... utilise ensuite merchantId
    }
}

Un bean singleton est partagé dans le conteneur concerné. Si un champ mutable currentMerchant reçoit le marchand de chaque demande, deux threads peuvent se remplacer cette valeur pendant leur traitement. Le second marchand peut alors être utilisé pour une opération du premier.

Conservez le contexte propre à l’appel dans une variable locale ou une commande immuable, puis transmettez-le explicitement aux dépendances. Ce modèle rend la relation visible et évite de stocker une identité ambiante dans un objet partagé. Changer simplement le nom du champ ou rendre sa lecture plus rapide ne résout pas la course.

Un test concurrent synchronisé peut suspendre A après l’affectation, faire avancer B, puis reprendre A pour révéler le mélange. Vérifiez références prestataire et tenant sur les appels factices. Une structure immuable reste insuffisante si elle contient un objet interne mutable partagé ; examinez les références qui composent réellement le contexte et gardez l’autorisation à la frontière métier.

Points de vigilance : L’état propre à une requête ne doit pas vivre dans un champ mutable partagé entre traitements. volatile apporte de la visibilité mémoire, pas une isolation par requête. Un champ static élargit le partage au lieu de le supprimer. L’isolation transactionnelle des données ne garantit pas celle des champs en mémoire.

Reference documentation ↗

Worked example

try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
    var result = executor.submit(() -> "R1");
    System.out.println(result.get());
}

Le résultat est attendu et les erreurs d’exécution ne sont pas ignorées. La fermeture de l’exécuteur traite sa terminaison selon son contrat.

Practical exercise

Simulez cent appels bloquants, puis imposez au plus cinq accès simultanés à une dépendance limitée.

View a suggested solution

Utilisez un Semaphore autour de l’accès rare avec libération dans finally. Lancez les tâches dans l’exécuteur, attendez leurs Future et vérifiez le maximum observé.

Verification checklist

  • Tester la concurrence avec plusieurs tâches réelles.
  • Observer les exceptions des Future.
  • Ne pas confondre visibilité et opération atomique.

Knowledge check

Related lesson: Concurrence, exécuteurs et virtual threads ↗
volatile int count rend-il count++ atomique ?

Reference documentation ↗

Module validation

101 questions, local practice target of 75%. Suggested duration of 150 minutes, with no enforced deadline. Questions and choices are shuffled; corrections appear after submission. The quick lesson check and the final course exam remain separate exercises.

Start or resume the 101 questions →