Ce document de Travaux Pratiques (TP N°4), intitulé « Moniteurs – Correction », s'adresse aux étudiants de 1ère année Ingénieur suivant le module de Systèmes d'Exploitation Avancés. Il a pour objectif d'illustrer et de corriger des notions essentielles liées à la concurrence et à la synchronisation des processus.
Les notions clés couvertes sont :
- L'allocation de ressources à l'aide de moniteurs Java.
- La synchronisation de threads pour la gestion de séquences, comme la production alternée de nombres pairs et impairs.
Ces exemples concrets en Java visent à renforcer la compréhension des mécanismes de coordination des processus concurrents.
Correction du TP N°4 : Moniteurs et Synchronisation en Java
Ce document présente la correction du Travaux Pratiques (TP) numéro 4, axé sur l'implémentation des moniteurs en Java pour gérer la concurrence et la synchronisation entre threads. Deux cas d'étude sont abordés : un allocateur de ressources et un mécanisme de synchronisation pour l'affichage de nombres pairs et impairs.
Partie 1 : Allocateur de ressources avec moniteurs
Cette section démontre comment utiliser les moniteurs pour gérer l'allocation et la libération de ressources partagées entre plusieurs processus (threads), garantissant ainsi un accès sécurisé et ordonné.
Classe Processus
La classe Processus représente un thread qui demande et libère des ressources de manière aléatoire. Chaque instance de cette classe est un consommateur de ressources potentielles.
package resources;
public class Processus extends Thread {
private Ressource ressources;
public Processus(Ressource ressources) {
this.ressources = ressources;
}
public void run() {
int m = (int)Math.random() * 10; // Demande un nombre aléatoire de ressources (entre 0 et 9)
try {
ressources.request(m); // Demande les ressources via la méthode synchronisée
sleep((long) (Math.random() * 200)); // Simule une période d'utilisation des ressources
} catch (InterruptedException e) {
e.printStackTrace(); // Gère l'interruption du thread
}
ressources.release(m); // Libère les ressources après utilisation
}
}
Dans cette implémentation, un Processus demande un nombre aléatoire de ressources, simule leur utilisation, puis les libère. La gestion de la concurrence est déléguée à l'objet Ressource.
Classe Ressource
La classe Ressource gère un pool de ressources disponibles et assure que les demandes d'allocation et de libération sont traitées de manière synchronisée. Elle utilise les mécanismes de wait() et notifyAll() pour la coordination entre threads.
package resources;
public class Ressource {
public int dispo = 10; // Nombre initial de ressources disponibles
public synchronized void request(int m) throws InterruptedException {
// Tant que les ressources disponibles sont inférieures à la quantité demandée
while (dispo < m) {
System.out.println("**** " + Thread.currentThread().getName()
+ " en veut " + m + "... doit attendre :(");
wait(); // Le thread se met en attente et libère le verrou du moniteur
}
// Une fois la condition remplie, les ressources sont allouées
System.out.println("Le demandeur " + Thread.currentThread().getName()
+ " utilise " + m + " ressources!");
dispo -= m; // Décrémente les ressources disponibles
System.out.println("Ressources disponibles : " + dispo);
}
public synchronized void release(int m) {
// Libération des ressources
System.out.println("Le demandeur " + Thread.currentThread().getName()
+ " libère " + m + " ressources!");
dispo += m; // Incrémente les ressources disponibles
System.out.println("Ressources disponibles : " + dispo);
notifyAll(); // Notifie tous les threads en attente qu'une ressource a été libérée
}
}
Les méthodes request et release sont déclarées synchronized, ce qui garantit qu'un seul thread peut accéder à l'état interne de Ressource à un instant donné. La boucle while (dispo < m) avec wait() est un modèle courant pour les moniteurs, assurant que le thread attend jusqu'à ce que la condition nécessaire soit satisfaite, même après un réveil intempestif.
Classe Main (pour l'allocateur de ressources)
Cette classe est le point d'entrée du programme. Elle initialise l'objet Ressource partagé et lance plusieurs threads Processus pour démontrer l'allocation concurrente.
package resources;
public class Main {
public static void main(String[] args) {
int nbThr = 5; // Nombre de threads Processus à créer
Ressource ressource = new Ressource(); // Création de l'objet ressource partagé
for (int i = 0; i < nbThr; i++){
Processus p = new Processus(ressource);
p.start(); // Lancement de chaque thread Processus
}
}
}
L'exécution de cette classe lancera cinq threads Processus qui interagiront avec la même instance de Ressource, illustrant la gestion sécurisée des ressources partagées.
Partie 2 : Synchronisation de compteur pair/impair avec moniteurs
Cette partie présente un exemple classique de synchronisation de threads pour afficher des nombres pairs et impairs alternativement, en utilisant un moniteur pour coordonner leur exécution séquentielle.
Classe SynchroClass
Cette classe sert de moniteur partagé entre les threads qui impriment des nombres pairs et impairs. Elle contient un drapeau, tour, qui détermine quel thread a le droit de s'exécuter.
package synchropairimpairthread;
public class SynchroClass {
public int tour = 0; // 0 pour le thread pair, 1 pour le thread impair
}
La variable tour est l'élément central de la synchronisation : sa valeur indique quel thread doit prendre le contrôle pour la prochaine itération.
Classe Main (pour la synchronisation pair/impair)
Cette classe initialise le moniteur SynchroClass et les deux threads (un pour les nombres impairs et un pour les pairs), puis les démarre pour commencer la séquence d'affichage synchronisée.
package synchropairimpairthread;
public class Main {
public static void main(String[] args) {
SynchroClass synchronizer = new SynchroClass(); // L'objet moniteur partagé
ImpairThread thimp = new ImpairThread(1, synchronizer); // Commence à 1 (impair)
PairThread thp = new PairThread(0, synchronizer); // Commence à 0 (pair)
thp.start(); // Lance le thread pair
thimp.start(); // Lance le thread impair
}
}
Les threads PairThread et ImpairThread reçoivent la même instance de SynchroClass, ce qui leur permet de communiquer et de se synchroniser.
Classe PairThread
Le thread PairThread est chargé d'afficher des nombres pairs. Il utilise le moniteur SynchroClass pour s'assurer qu'il n'imprime son nombre que lorsqu'il est son tour, puis il passe la main au thread impair.
package synchropairimpairthread;
public class PairThread extends Thread {
private int début; // Le nombre de départ (doit être pair)
private SynchroClass sync; // L'objet moniteur partagé
public PairThread(int start, SynchroClass s) {
début = start;
sync = s;
}
public void run() {
while(true) {
synchronized(sync) { // Verrouille l'objet sync pour la synchronisation
try {
if (sync.tour != 0) { // Si ce n'est pas le tour du thread pair
sync.wait(); // Le thread attend et libère le verrou
}
System.out.print(" " + début); // Affiche le nombre pair
début += 2; // Incrémente pour le prochain nombre pair
sync.tour = 1; // Passe la main au thread impair
sync.notify(); // Notifie un thread en attente (normalement ImpairThread)
} catch (InterruptedException ex) {
// La gestion de l'exception est minimale ici pour la démonstration
}
}
}
}
}
Le bloc synchronized(sync) assure que seul un thread à la fois peut modifier l'état de sync ou appeler ses méthodes wait()/notify(). Le wait() est conditionnel à sync.tour != 0, garantissant que le thread attend uniquement si ce n'est pas son tour.
Classe ImpairThread
Similaire au PairThread, le ImpairThread est responsable de l'affichage des nombres impairs, attendant son tour grâce au moniteur partagé et notifiant l'autre thread après avoir effectué son action.
package synchropairimpairthread;
public class ImpairThread extends Thread {
private int début; // Le nombre de départ (doit être impair)
private SynchroClass sync; // L'objet moniteur partagé
public ImpairThread(int start, SynchroClass s) {
début = start;
sync = s;
}
public void run() {
while(true) {
synchronized(sync) { // Verrouille l'objet sync pour la synchronisation
try {
if (sync.tour != 1) { // Si ce n'est pas le tour du thread impair
sync.wait(); // Le thread attend et libère le verrou
}
System.out.print(" " + début); // Affiche le nombre impair
début += 2; // Incrémente pour le prochain nombre impair
sync.tour = 0; // Passe la main au thread pair
sleep(1000); // Ajoute un délai pour une meilleure observation
sync.notify(); // Notifie un thread en attente (normalement PairThread)
} catch (InterruptedException ex) {
// La gestion de l'exception est minimale ici pour la démonstration
}
}
}
}
}
Un sleep(1000) a été intentionnellement ajouté dans le ImpairThread pour ralentir l'exécution et rendre l'alternance des affichages plus facilement observable dans la console. Cela aide à visualiser le mécanisme de synchronisation en action.
Foire Aux Questions (FAQ) sur les Moniteurs en Java
- Qu'est-ce qu'un moniteur en Java et à quoi sert-il ?
- En Java, un moniteur est un mécanisme de synchronisation intégré qui permet de gérer l'accès concurrent aux ressources partagées. Il est principalement implémenté via le mot-clé
synchronizedpour les méthodes ou les blocs de code, et les méthodeswait(),notify(),notifyAll()de la classeObject. Son objectif est d'assurer l'exclusion mutuelle (un seul thread accède à la section critique à la fois) et la coordination entre threads qui doivent attendre certaines conditions. - Quelle est la différence fondamentale entre
wait()etsleep()en Java ? - Les deux méthodes suspendent l'exécution d'un thread, mais leur comportement est crucialement différent. La méthode
wait(), appelée sur un objet, libère le verrou du moniteur associé à cet objet et met le thread en attente jusqu'à ce qu'il soit notifié ou interrompu. Elle doit être appelée dans un blocsynchronized. En revanche, la méthode statiqueThread.sleep()ne libère PAS le verrou du moniteur. Elle suspend simplement le thread pour une durée spécifiée, quel que soit l'état des verrous, et peut être appelée n'importe où. - Pourquoi est-il recommandé d'utiliser
notifyAll()plutôt quenotify()dans l'exemple de l'allocateur de ressources ? - Dans l'allocateur de ressources, plusieurs threads peuvent être en attente de ressources (c'est-à-dire bloqués sur
wait()). Sinotify()est utilisé, seul un thread parmi ceux en attente est réveillé. Ce thread pourrait ne pas être celui dont la condition (dispo < m) est désormais satisfaite, ou il pourrait avoir besoin de plus de ressources que celles qui ont été libérées. Cela peut entraîner une situation où le "bon" thread reste bloqué tandis qu'un autre est réveillé inutilement.notifyAll()réveille tous les threads en attente, leur permettant à chacun de revérifier leur condition et de procéder si elle est remplie, ce qui est plus robuste pour la gestion des ressources multiples.