# Architecture des threads Java et synchronisation : du CPU à la JVM
Le code Java est compilé en bytecode, que la JVM convertit en instructions machine pour le processeur. Chaque cœur de CPU exécute les instructions de manière séquentielle : une opération simple comme un décalage de bits dans un registre se fait en un seul cycle d'horloge. Les registres sont des cellules de mémoire ultra-rapides pour stocker des nombres, des adresses et des données.
Les processeurs modernes utilisent une architecture superscalaire. L'Hyper-Threading permet à un seul thread matériel d'exécuter 2 à 6 instructions par cycle, créant l'illusion de plusieurs threads logiques par cœur grâce à un basculement de contexte rapide.
Le nombre de threads s'exécutant simultanément est limité par le nombre de cœurs. Les threads partagent la mémoire tas pour les objets, tandis que chacun dispose de sa propre pile pour les variables locales et les références d'objets.
Conditions de course et structure de la mémoire
Une condition de course se produit lorsque plusieurs threads lisent et modifient le même objet du tas en même temps. Le résultat est imprévisible, dépendant de l'ordre des opérations lecture-modification-écriture.
Le code fonctionne souvent parfaitement en local sans charge, mais en production sous multitâche, des collisions apparaissent de manière irrégulière. Un processus OS alloue de la mémoire partagée ; les threads qu'il contient sont des unités d'exécution légères en compétition pour les ressources.
Mécanismes de synchronisation : wait/notify et moniteurs
Chaque objet Java possède un moniteur intégré (verrou). Le bloc synchronized acquiert le moniteur de l'objet, garantissant :
- Exclusion mutuelle : Un seul thread exécute le code à l'intérieur du bloc.
- Visibilité : Les modifications sont visibles pour les autres threads après la sortie du bloc (avec le support de
volatile).
Le moniteur comprend :
- Entry Set : File d'attente des threads attendant de l'acquérir.
- Wait Set : Threads en état d'attente.
Les méthodes wait(), notify() et notifyAll() ne fonctionnent qu'à l'intérieur d'un bloc synchronized sur le même objet — sinon, vous obtenez une IllegalMonitorStateException.
wait(): Déplace le thread vers le Wait Set.notify(): Réveille un thread aléatoire (risque de réveil perdu).notifyAll(): Réveille tous les threads (approche préférée).
Les méthodes synchronized statiques verrouillent le moniteur de la classe (Class), sans conflit avec les méthodes d'instance (this).
Verrouillage avancé : ReentrantLock et Semaphore
Pour plus de contrôle, utilisez java.util.concurrent.locks.ReentrantLock :
Lock lock = new ReentrantLock();
lock.lock();
try {
// section critique
} finally {
lock.unlock();
}
Méthodes clés :
tryLock(): Acquisition sans attente (retourne false en cas d'échec).tryLock(timeout, unit): Attente avec délai.lockInterruptibly(): Acquisition interrompable.new ReentrantLock(true): Verrouillage équitable (ordre FIFO, plus lent).
Semaphore limite l'accès :
Semaphore sem = new Semaphore(3); // 3 permis
sem.acquire(); // acquisition
sem.release(); // libération
Pièges du multitâche : verrou mort et verrou vivant
Verrou mort : Dépendances cycliques de verrous. Le thread A tient lock1 et attend lock2 ; le thread B fait l'inverse. Le système se fige.
Pour l'éviter :
- Verrouiller dans un ordre fixe.
- Utiliser des délais.
- Minimiser les verrous imbriqués.
Verrou vivant : Les threads sont occupés mais n'avancent pas. Exemple : deux threads se cèdent poliment les ressources en boucle, faisant monter le CPU sans progresser.
Synchrone vs asynchrone dans les threads
Les méthodes synchrones bloquent le thread sur les E/S (ex. : requête BD) : le thread reste inactif jusqu'à la réponse.
L'asynchrone utilise CompletableFuture — un conteneur pour des résultats différés (en attente, terminés ou échoués).
Chaînes d'opérations :
thenApply(): Transformer le résultat.thenCompose(): Composer avec un autre Future.thenAccept(): Consommer sans retour.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "result")
.thenApply(s -> s.toUpperCase())
.thenAccept(System.out::println);
Cela libère le thread pour d'autres tâches, augmentant le débit dans les environnements multitâches.
Points clés à retenir
- Chaque cœur exécute un thread matériel ; l'Hyper-Threading simule plusieurs.
- Les conditions de course proviennent du tas partagé ; la synchronisation assure l'exclusion mutuelle et la visibilité.
- Préférez
notifyAll()ànotify()pour éviter les réveils perdus. - ReentrantLock étend
synchronizedavec des délais, interruptions et équité. - CompletableFuture permet des pipelines asynchrones sans bloquer les threads.
— Editorial Team
Aucun commentaire pour le moment.