Badr Benmammar bbm@badr-benmammar.com Programmation concurrente et temps réel en Java Badr Benmammar bbm@badr-benmammar.com
Cycle de vie d’un Thread class BavarderEtLancerLePerroquet4 { public static void main(String args[]) { Perroquet4 perroquet = new blabla(); Perroquet4("coco",5); perroquet.start(); System.out.println ("Thread bavard : " + Thread.currentThread().getName()); for (int n=0; n<15; n++) { try { Thread.sleep(1000); } catch(InterruptedException e) { } System.out.println("Thread perroquet isAlive : " + perroquet.isAlive()); private static void blabla() { System.out.println("blabla"); class Perroquet4 extends Thread { private String cri = null; private int fois = 0; public Perroquet4 (String s, int i) { cri = s; fois = i; } public void run() { System.out.println("Thread perroquet : " + Thread.currentThread().getName()); for (int n=0; n<fois; n++) { try { Thread.sleep(1000); catch(InterruptedException e) { } System.out.println(cri);
Cycle de vie d’un Thread Exécution: Thread bavard : main Thread perroquet : Thread-0 Thread perroquet isAlive : true blabla coco Thread perroquet isAlive : false blabla Le Thread "principal", celui qui exécute main s'appelle main. L’autre, celui du perroquet, a reçu comme nom par défaut Thread-0. La méthode isAlive() détermine si un Thread est en train d'exécuter sa méthode run. La méthode de classe currentThread() retourne un pointeur sur l’objet Thread qui appelle cette méthode. La méthode getName() donne le nom du Thread.
Propriétés des différents Threads class BavarderEtLancerLePerroquet5 { public static void main(String args[]) { Thread.currentThread().setName("bavard"); Perroquet5 perroquet = new Perroquet5("coco",15); perroquet.start(); for (int n=0; n<5; n++) { try { Thread.sleep(1000); } catch(InterruptedException e) { } blabla(); }} private static void blabla() { System.out.println("blabla"); }} class Perroquet5 extends Thread { private String cri = null; private int fois = 0; public Perroquet5(String s, int i) { super("perroquet"); cri = s; fois = i; } public void run(){ afficheThreads(); for (int n=0; n<fois; n++) { try { Thread.sleep(1000); } catch(InterruptedException e) { } System.out.println(cri); } } private void afficheThreads() { Thread[] tabThread = new Thread[Thread.activeCount()]; int nbrThread = Thread.enumerate(tabThread); for (int i = 0; i < nbrThread ; i++) System.out.println(i + "-ieme Thread : " + tabThread[i].getName());
Propriétés des différents Threads Exécution: 0-ieme Thread : bavard 1-ieme Thread : perroquet coco blabla 0-ieme Thread : perroquet 1-ieme Thread : DestroyJavaVM La méthode setName (String nom) permet de nommer le Thread. Le constructeur offre la possibilité de le nommer ainsi Thread (String nom). La méthode de classe activeCount () donne le nombre de Threads actifs dans le groupe de l'appelant. La méthode de classe enumerate (Thread[] tableau) stocke dans le tableau donné les références des Threads actifs dans le groupe de l'appelant et ses sous-groupes. Elle renvoie le nombre de Threads actifs obtenus. Remarquons que le Thread main, renommé bavard, a fini avant le Thread perroquet, et ne l'attend pas pour terminer.
Synchronisation sur terminaison class BavarderEtLancerLePerroquet6{ public static void main(String args[]) { Perroquet6 perroquet = new Perroquet6("coco",10); perroquet.start(); for (int n=0; n<5; n++) blabla(); try { perroquet.join(); } catch(InterruptedException e) { System.out.println(e.getMessage()); System.exit(2); } System.out.println("fin du Thread perroquet !"); } private static void blabla() { System.out.println("blabla"); try { Thread.sleep(1000); } catch(InterruptedException e) { } }} Exécution: blabla coco fin du Thread perroquet ! class Perroquet6 extends Thread { private String cri = null; private int fois = 0; public Perroquet6 (String s, int i) { super ("perroquet"); cri = s; fois = i; } public void repeter() { System.out.println(cri); try {Thread.sleep(1000); } catch(InterruptedException e) { } } public void run() { for (int n=0; n<fois; n++) repeter();
Synchronisation sur terminaison Exécution: blabla coco fin du Thread perroquet ! La méthode join() attend la terminaison du Thread spécifié. Si le Thread est créé mais pas "starté", il est considéré comme terminé ! join() peut aussi avoir un paramètre donné qui est un timeout maximal. Les 2 Threads ne sont plus indépendants puisque le "main" attend, à une certaine étape, la terminaison (fin d'exécution) de l'autre. C'est une forme de synchronisation. perroquet.join(); Exécution en multitâche Le main attend la terminaison du Thread perroquet Le main termine son exécution
Exemple de join() : synchroniser deux écrivains public class Ecrivain extends Thread { private String texte; public Ecrivain(String t) { texte=t; } public void run() { for (int i=0; i<10; i++) { int j=0; for (;j<texte.length()-1; j++) { System.out.print(texte.substring(j,j+1)); try { sleep((long)(Math.random() * 100)); } catch (InterruptedException e) {} System.out.println(texte.substring(j,j+1)); System.out.println("ecrivain de " +texte+" a fini"); public class Prog55 { public static void main (String argv[]) { Ecrivain ecrivainA, ecrivainB; ecrivainA = new Ecrivain ("ABC"); ecrivainB = new Ecrivain ("XYZ"); ecrivainA.start(); ecrivainB.start(); } Exemple de substring : String date = "15/08/2000"; String jour = date.substring(0,2);// donne 15 String mois = date.substring(3,5); // donne 08 String annee = date.substring(6,10); // donne 2000 Chaine du caractère 6 à 9 (premier caractère d’indice 0).
Exemple de join() : synchroniser deux écrivains Exécution: AXBYC ABZ XC AYZ XBC AYBC AZ ABYZ ecrivain de ABC a fini YZ XYZ ecrivain de XYZ a fini Exécution : ABC ecrivain de ABC a fini XYZ ecrivain de XYZ a fini public class Prog55 { public static void main (String argv[]) { Ecrivain ecrivainA, ecrivainB; ecrivainA = new Ecrivain ("ABC"); ecrivainB = new Ecrivain ("XYZ"); ecrivainA.start(); try { ecrivainA.join(); } catch(InterruptedException e) { System.out.println(e.getMessage()); System.exit(1); ecrivainB.start();
Deux Threads sans sleep Exécution: blabla coco class BavarderEtLancerLePerroquet7 { public static void main(String args[]) { Perroquet7 perroquet = new Perroquet7("coco",10); perroquet.start(); for (int n=0; n<10; n++) blabla(); } private static void blabla() { System.out.println("blabla"); class Perroquet7 extends Thread { private String cri = null; private int fois = 0; public Perroquet7(String s, int i) { cri = s; fois = i; } public void repeter() { System.out.println(cri); } public void run() { for (int n=0; n<fois; n++) repeter(); Le temps d’exécution est trop court pour visualiser la répartition du CPU entre les 2 Threads.
Répartition de temps entre deux Threads public class CourseInfernale1 { public static void main(String[] args) { Coureur A = new Coureur("A"); Coureur B = new Coureur("B"); A.start(); B.start(); }} class Coureur extends Thread { String nom; public Coureur(String nom) { super(nom); this.nom = nom; } public void run() { long coupsDePedale = 0; while (coupsDePedale < 5000000) { coupsDePedale++; if ((coupsDePedale % 500000) == 0) { System.out.println("Coureur " + nom + " a donne " + coupsDePedale + " coups de pedale."); } } }}
Répartition de temps entre deux Threads Exécution: Coureur A a donne 500000 coups de pedale. Coureur B a donne 500000 coups de pedale. Coureur B a donne 1000000 coups de pedale. Coureur A a donne 1000000 coups de pedale. Coureur B a donne 1500000 coups de pedale. Coureur A a donne 1500000 coups de pedale. Coureur A a donne 2000000 coups de pedale. Coureur B a donne 2000000 coups de pedale. Coureur A a donne 2500000 coups de pedale. Coureur B a donne 2500000 coups de pedale. Coureur A a donne 3000000 coups de pedale. Coureur B a donne 3000000 coups de pedale. Coureur A a donne 3500000 coups de pedale. Coureur B a donne 3500000 coups de pedale. Coureur A a donne 4000000 coups de pedale. Coureur B a donne 4000000 coups de pedale. Coureur A a donne 4500000 coups de pedale. Coureur B a donne 4500000 coups de pedale. Coureur A a donne 5000000 coups de pedale. Coureur B a donne 5000000 coups de pedale. Java n'impose pas que le système soit "time-sliced" : cad que la même quantité de temps soit impartie aux Threads de même niveau de priorité.
Priorité entre Threads La méthode setPriority fixe le niveau de priorité entre les différents Threads. La valeur doit être comprise entre une valeur minimale, MIN_PRIORITY, et maximale, MAX_PRIORITY. class Coureur extends Thread { String nom; public Coureur(String nom) { super(nom); this.nom = nom; } public void run() { long coupsDePedale = 0; while (coupsDePedale < 5000000) { coupsDePedale++; if ((coupsDePedale % 500000) == 0) { System.out.println("Coureur " + nom + " a donne " + coupsDePedale + " coups de pedale."); } public class CourseInfernale2 { public static void main(String[] args) { Coureur A = new Coureur("A"); Coureur B = new Coureur("B"); A.setPriority(Thread.MAX_PRIORITY); B.setPriority(Thread.MIN_PRIORITY); System.out.println("Thread Coureur " + A.nom + " a la priorite = " + A.getPriority()); B.nom + " a la priorite = " + B.getPriority()); A.start(); B.start(); }
Priorité entre Threads Exécution: Thread Coureur A a la priorite = 10 Thread Coureur B a la priorite = 1 Coureur A a donne 500000 coups de pedale. Coureur A a donne 1000000 coups de pedale. Coureur A a donne 1500000 coups de pedale. Coureur A a donne 2000000 coups de pedale. Coureur A a donne 2500000 coups de pedale. Coureur A a donne 3000000 coups de pedale. Coureur A a donne 3500000 coups de pedale. Coureur A a donne 4000000 coups de pedale. Coureur A a donne 4500000 coups de pedale. Coureur A a donne 5000000 coups de pedale. Coureur B a donne 500000 coups de pedale. Coureur B a donne 1000000 coups de pedale. Coureur B a donne 1500000 coups de pedale. Coureur B a donne 2000000 coups de pedale. Coureur B a donne 2500000 coups de pedale. Coureur B a donne 3000000 coups de pedale. Coureur B a donne 3500000 coups de pedale. Coureur B a donne 4000000 coups de pedale. Coureur B a donne 4500000 coups de pedale. Coureur B a donne 5000000 coups de pedale.
Méthode yield() La méthode yield() "rend le processeur" : elle indique au contrôleur d'exécution des Threads d'en choisir un nouveau à exécuter (donc ca pourrait être le même !). Elle est intéressante dans peu de cas. class BavarderEtLancerLePerroquet8 { public static void main(String args[]) { Perroquet8 perroquet = new Perroquet8("coco",10); perroquet.start(); for (int n=0; n<10; n++) { blabla(); Thread.currentThread().yield(); } private static void blabla() { System.out.println("blabla"); class Perroquet8 extends Thread { private String cri = null; private int fois = 0; public Perroquet8(String s, int i) { cri = s; fois = i; } public void repeter() { System.out.println(cri); } public void run() { for (int n=0; n<fois; n++) { repeter(); yield(); }
Essayer de Stopper l’exécution d’un Thread class LancerEtArreterLePerroquet9 { public static void main(String args[]) { Perroquet9 perroquet = new Perroquet9("coco"); perroquet.start(); String reponse="o"; do {System.out.println("voulez-vous que le perroquet continue ? (o/n)"); Thread.currentThread().yield(); reponse = Saisie.litexte(); } while (reponse.equals("o")); }} Exécution: coco … class Perroquet9 extends Thread { private String cri = null; public Perroquet9(String s) { cri = s; } public void repeter() { System.out.println(cri); try { Thread.sleep((int)Math.random()*1000); catch(InterruptedException e) {} public void run() { while (true) { repeter(); yield(); } }} L’arrêt de l'exécution du Thread main n'entraine pas l'arrêt du Thread perroquet. L'arrêt brutal d’un Thread pouvait laisser des objets dans des états inconsistants (par exemple, des verrous pouvaient avoir été posés). C'est au programmeur de prévoir quand (et donc comment) l'exécution du Thread peut s'arrêter sans risque.
Stopper proprement : mot clé volatile class LancerEtArreterLePerroquet10{ public static void main(String args[]) { Perroquet10 perroquet = new Perroquet10("coco"); perroquet.start(); String reponse="o"; do { System.out.println("voulez-vous que le perroquet continue ? (o/n)"); reponse = Saisie.litexte(); } while (reponse.equals("o")); perroquet.stopper(); class Perroquet10 extends Thread{ private volatile boolean continuer = true; private String cri = null; public Perroquet10(String s) { continuer = true; cri = s; } public void repeter() { System.out.println(cri); try { Thread.sleep((int)Math.random()*2000); } catch(InterruptedException e) { } public void stopper() { continuer = false;} public void run() { while (continuer) { repeter(); } }
Stopper proprement Exécution : coco ncoco La solution est très simple : une variable booléenne sert dans la méthode run pour savoir s’il faut continuer ou arrêter. Cette variable doit être déclarée volatile. Pour une variable, le modificateur volatile force la JVM, avant et après chaque utilisation de la variable par un Thread , à la rafraîchir à partir de la mémoire principale au lieu d'utiliser un cache local. Cela permet de synchroniser la valeur de la variable entre plusieurs Threads.
Programmer une tâche en précisant un délai initial class Perroquet11 extends TimerTask { private String cri = null; private int fois = 0; public Perroquet11(String s, int i) { cri = s; fois = i; } public void repeter() { System.out.println(cri); public void run() { for (int i = 0; i < fois; i++) { repeter(); import java.util.TimerTask; import java.util.Timer; class DeclancherLePerroquet11{ public static void main(String args[]) { Perroquet11 perroquet = new Perroquet11("coco", 3); Timer timer = new Timer(); timer.schedule(perroquet, 4000); String reponse="oui"; do { System.out.println("blabla"); System.out.println("voulez-vous encore bavarder ? (o/n)"); reponse = Saisie.litexte(); } while (reponse.equals("o")); timer.cancel();
TimerTask public abstract class TimerTask extends Object implements Runnable TimerTask est une classe abstraite qui implémente Runnable, donc une méthode run(), il faut donc hériter de la classe TimerTask et redéfinir la méthode run() qui code la tâche à effectuer. Un Timer permet de déclencher l'exécution de tâches une ou plusieurs fois en précisant un délai initial et/ou une périodicité. Plusieurs tâches peuvent être programmées selon des programmes divers.
Programmer une tâche en précisant un délai initial Exécution: blabla voulez-vous encore bavarder ? (o/n) o coco n A un Timer correspond un Thread qui exécutera successivement les tâches à effectuer. Les tâches sont des TimerTask et doivent être courte. Aucune garantie de temps réel n'est assurée par ce mécanisme. schedule(tache, long millisecondes) programme la tâche en précisant un délai initial en millisecondes. La méthode cancel() arrête la programmation du Timer.
Programmer une tâche avec délai initial et une périodicité class Perroquet11 extends TimerTask { private String cri = null; private int fois = 0; public Perroquet11(String s, int i) { cri = s; fois = i; } public void repeter() { System.out.println(cri); public void run() { for (int i = 0; i < fois; i++) { repeter(); import java.util.TimerTask; import java.util.Timer; class DeclancherLePerroquet11{ public static void main(String args[]) { Perroquet11 perroquet = new Perroquet11("coco", 3); Timer timer = new Timer(); timer.schedule(perroquet, 3000,2000); String reponse="oui"; do { System.out.println("blabla"); System.out.println("voulez-vous encore bavarder ? (o/n)"); reponse = Saisie.litexte(); } while (reponse.equals("o")); timer.cancel(); schedule(tache, long delai, long période) programme la tâche après un délai pour une exécution périodiques : les temps sont donnés en millisecondes.
Programmer une tâche avec délai initial et une périodicité Exécution: blabla voulez-vous que le perroquet continue ? (o/n) coco ncoco
Partager une ressource: imprimeur public class Imprimeur1 { private String texte; public Imprimeur1() { texte=""; } public void imprimer(String t) { texte=t; for (int j=0;j<texte.length()-1; j++) { System.out.print(texte.substring(j,j+1)); try { Thread.sleep(100); } catch (InterruptedException e) {}; } System.out.println (texte.substring(texte.length()-1,texte.length())); }} public class Ecrivain2 extends Thread { private String texte; private Imprimeur1 imprim; public Ecrivain2(String t, Imprimeur1 i) { imprim=i; texte=t; } public void run() { for (int i=0; i<10; i++) { imprim.imprimer(texte); try { sleep((long)(Math.random() * 100)); } catch (InterruptedException e) {} System.out.println("ecrivain de " +texte+" a fini"); public class Prog56 { public static void main (String argv[]) { Ecrivain2 ecrivainA, ecrivainB; Imprimeur1 imprim= new Imprimeur1(); ecrivainA = new Ecrivain2("ABC", imprim); ecrivainB = new Ecrivain2("XYZ", imprim); ecrivainA.start(); ecrivainB.start(); }}
Partager une ressource: imprimeur Exécution: AXYYZ Z AC XYYZ XABBC C ecrivain de XYZ a fini ecrivain de ABC a fini Les écrivains passent par un imprimeur pour écrire. Les 2 écrivains s'adressent maintenant à un imprimeur commun. Non seulement, les écrivains écrivent en "interleaving" (entrelacement), mais ils "écrasent" la variable texte de l'imprimeur. L'ensemble est encore illisible.
Ressource en exclusion mutuelle public class Imprimeur1 { private String texte; public Imprimeur1() { texte=""; } public synchronized void imprimer(String t) { texte=t; for (int j=0;j<texte.length()-1; j++) { System.out.print(texte.substring(j,j+1)); try { Thread.sleep(100); } catch (InterruptedException e) {}; } System.out.println (texte.substring(texte.length()-1,texte.length()));
Ressource en exclusion mutuelle Exécution : ABC XYZ Xecrivain de ABC a fini YZ ecrivain de XYZ a fini synchronized définit un verrou/ une section en exclusion mutuelle sur la méthode imprimer : Un seul Thread au plus peut exécuter la méthode à la fois.
Synchronisation des threads
Plan Variables partagées Problème de l’exclusion mutuelle Bloc synchronisé Méthode d’instance synchronisée Problème de coopération des threads wait, notifyAll et notify Demi-synchronisation (wait et sleep) Problème du Producteur et du Consommateur Producteurs-Consommateurs Sémaphore Interblocage
Variables partagées class Perroquet20 extends Thread { private String cri = null; private int fois = 0; public Perroquet20 (String s, int i) { cri = s; fois = i; } public void repeter() { String repete = cri + " " + compteur; System.out.println(repete); compteur++; try { Thread.sleep((int)(Math.random()*1000)); catch(InterruptedException e) { } public void run(){ for (int n=0; n<fois; n++) repeter(); }} class PerroquetsMatheux20 { private int compteur; public static void main(String args[]) { new PerroquetsMatheux20(); } public PerroquetsMatheux20 () { compteur = 1; Perroquet20 perroquetA = new Perroquet20("coco", 10); Perroquet20 perroquetB = new Perroquet20("bonjour", 10); perroquetA.start(); perroquetB.start(); try { perroquetA.join(); perroquetB.join(); catch(InterruptedException e) { } System.out.println("compteur = "+compteur);
Variables partagées Exécution: coco 1 bonjour 2 bonjour 3 coco 4 bonjour 5 coco 6 bonjour 7 bonjour 8 bonjour 9 coco 10 bonjour 11 coco 12 bonjour 13 coco 14 coco 15 bonjour 16 coco 17 bonjour 18 coco 19 coco 20 compteur = 21 La classe Perroquet20 se situe à l’intérieur de la classe PerroquetsMatheux20, et la méthode join est utilisée afin de ne pas afficher la valeur du compteur avant la terminaison des deux Threads. Du fait des règles de visibilité de Java, la variable compteur est visible/accessible à partir de la classe Perroquet20 donc des 2 objets threads perroquetA et perroquetB, par contre, les variables d'instance cri et fois de Perroquet20 existent en autant d'exemplaires que d'instances de Perroquet20. Les 2 threads accède donc à un espace partagé/commun de variables. Contrairement au processus qui possède son propre espace de travail clairement séparé des autres processus, les threads sont exécutés au sein du même processus "java".
Problème de l’accès concurrent (partage de ressource) class PerroquetsMatheux21{ private int compteur; public static void main(String args[]) { new PerroquetsMatheux21(); } public PerroquetsMatheux21() { compteur = 0; Perroquet21 perroquetA = new Perroquet21("coco", 10); Perroquet21 perroquetB = new Perroquet21("bonjour", 10); //perroquetA.setPriority(perroquetB.getPriority()%2); perroquetA.start(); perroquetB.start(); try { perroquetA.join(); perroquetB.join(); } catch(InterruptedException e) { } System.out.println("compteur = "+compteur); } class Perroquet21 extends Thread { private String cri = null; private int fois = 0; public Perroquet21(String s, int i) { cri = s; fois = i; } public void repeter() { int valeur = compteur + 1; String repete = cri + " " + valeur; System.out.println(repete); try { Thread.sleep((int)(Math.random()*100)); } catch(InterruptedException e) { } compteur = valeur; try {Thread.sleep((int)(Math.random()*100));} } public void run(){ for (int n=0; n<fois; n++) repeter();
Problème de l’accès concurrent (partage de ressource) Exécution: coco 1 bonjour 1 coco 2 coco 3 bonjour 3 coco 4 bonjour 4 coco 5 bonjour 5 coco 6 bonjour 7 coco 7 bonjour 8 coco 8 bonjour 9 coco 9 coco 10 bonjour 10 bonjour 11 bonjour 12 compteur = 12 Les 2 threads perroquet travaillent alternativement : un thread peut être suspendu au milieu de l’exécution de sa méthode repeter pour que le contrôleur de thread laisse l’autre s'exécuter.
Définir une section critique : Bloc synchronisé class PerroquetsMatheux22{ private Compteur compteur; public static void main(String args[]) { new PerroquetsMatheux22(); } public PerroquetsMatheux22() { compteur = new Compteur(); Perroquet22 perroquetA = new Perroquet22("coco", 10); Perroquet22 perroquetB = new Perroquet22("bonjour", 10); perroquetA.setPriority(perroquetB.getPriority()%2); perroquetA.start(); perroquetB.start(); try { perroquetA.join(); perroquetB.join(); } catch(InterruptedException e) { } System.out.println("compteur = "+compteur.getValeur()); } class Perroquet22 extends Thread { private String cri = null; private int fois = 0; public Perroquet22(String s, int i) { cri = s; fois = i; } public void repeter() { synchronized (compteur) { int valeur = compteur.getValeur() + 1; String repete = cri + " " + valeur; System.out.println(repete); try { Thread.sleep((int)(Math.random()*100)); } catch(InterruptedException e) { } compteur.setValeur(valeur); } try { Thread.sleep((int)(Math.random()*100)); } public void run(){ for (int n=0; n<fois; n++) repeter(); class Compteur { private int valeur = 0; public int getValeur() { return valeur; } public void setValeur(int v) { valeur = v; }
Définir une section critique : Bloc synchronisé Exécution: bonjour 1 coco 2 bonjour 3 coco 4 bonjour 5 coco 6 coco 7 bonjour 8 coco 9 bonjour 10 bonjour 11 coco 12 coco 13 bonjour 14 coco 15 bonjour 16 bonjour 17 coco 18 coco 19 bonjour 20 compteur = 20 Le mot-clé synchronized définit un bloc d'instruction qui ne peut s'exécuter qu'exclusivement même si plusieurs threads souhaitent l'exécuter : Lorsque le thread perroquetA exécute ce bloc synchronisé, et que le thread perroquetB souhaite commencer l'exécution de ce même bloc, alors le thread perroquetB doit attendre. Quand le thread perroquetA aura finit, le thread perroquetB pourra reprendre. Seul un thread à la fois peut exécuter un bloc synchronisé. On dit que le bloc est en exclusion mutuelle ou encore que c'est une section critique. Les autres threads, s'ils désirent exécuter cette section, doivent attendre que le thread en section critique la termine. Si plusieurs threads attendent pour un même bloc synchronisé qui "se libère", le contrôleur de thread n'en autorisera qu'un à l'exécuter. L'appel à sleep() ne provoque pas de sortie de la section critique.
Bloc synchronisé
Bloc synchronisé synchronized(objet) signifie que le bloc est en exclusion mutuelle relativement à un moniteur (monitor) de cet objet : sont en exclusion mutuelle, les threads synchronisés sur le même objet. Le thread de gauche et celui du milieu ont une section critique mutuelle, le thread de droite a une section critique mais pas avec les 2 autres threads. Le moniteur "tient" le rôle de superviseur s'assurant que seul un thread à la fois peut exécuter la section critique qu'il supervise : c'est un système de verrouillage (lock). Le thread qui exécute synchronized d'un objet devient propriétaire du moniteur de cet objet. Thread.sleep ne fait pas perdre la propriété d'un moniteur même temporairement. Il n'est pas souhaitable de mettre un sleep(délai) dans une zone synchronisée : on préfèrera wait(timeout).
Méthode d’instance synchronisée public void repeter() { int valeur = compteur.plus1(); String repete = cri + " " + valeur; System.out.println(repete); try { Thread.sleep((int)(Math.random()*100)); } catch(InterruptedException e) { } public void run(){ for (int n=0; n<fois; n++) repeter(); class Compteur { private int valeur = 0; public synchronized int plus1() { return ++valeur; class PerroquetsMatheux23 { private Compteur compteur; public static void main(String args[]) { new PerroquetsMatheux23(); } public PerroquetsMatheux23() { compteur = new Compteur(); Perroquet23 perroquetA = new Perroquet23("coco", 10); Perroquet23 perroquetB = new Perroquet23("bonjour", 10); perroquetA.setPriority(perroquetB.getPriority()%2); perroquetA.start(); perroquetB.start(); try { perroquetA.join(); perroquetB.join(); } catch(InterruptedException e) { } System.out.println("compteur = "+compteur.valeur); } class Perroquet23 extends Thread { private String cri = null; private int fois = 0; public Perroquet23(String s, int i) { cri = s; fois = i; }
Méthode d’instance synchronisée Exécution : coco 1 bonjour 2 bonjour 3 coco 4 coco 5 bonjour 6 bonjour 7 coco 8 bonjour 9 coco 10 bonjour 11 coco 12 bonjour 13 bonjour 14 coco 15 bonjour 16 coco 17 bonjour 18 coco 19 coco 20 compteur = 20 Le mot synchronised définit le bloc de la méthode en exclusion mutuelle içi c'est l'objet compteur qui "monitorise", la méthode synchronisée est aussi un mécanisme d'exclusion mutuelle sur une portion de code : le moniteur qui supervise cette section critique est celui de l'objet sur lequel est appelée la méthode. remarque : synchronized méthode(paramètres) { bloc d'instructions } La synchronisation ralentit l'ensemble de l'exécution, donc il faut limiter le nombre de portion synchronisée et leur taille (en instructions), il est possible de synchroniser sur une classe pour accéder en exclusion mutuelle sur les variables de classe, idem pour une méthode de classe synchronisée. Le mécanisme de "monitor" d'un objet s'applique à toutes les instances de Object : c'est donc un mécanisme implémenté au coeur de JAVA, un seul thread peut être à la fois le propriétaire du moniteur d'un objet. est équivalent à : méthode(paramètres) { synchronized(this) { bloc d'instructions } }
Rappel
Multitâche Un système d’exploitation est dit multitâche ou à temps partagé lorsque plusieurs «tâches» (processus) peuvent être exécutées simultanément. 2 types de système d’exploitation multitâche : Multitâche coopératif (non-préemptif) : Une forme simple de multitâche où chaque processus doit explicitement permettre à une autre tâche de s’exécuter. Cette approche simplifie l’architecture du système d’exploitation mais présente plusieurs inconvénients : Si un des processus ne redonne pas la main à un autre processus, par exemple si le processus est bugué, le système entier peut s’arrêter. Multitâche préemptif : Pour remédier à cette situation, les systèmes ont évolué pour utiliser une approche nommée « multitâche préemptif ». Dans un tel système, le processeur signale au système d’exploitation que le processus en cours d’exécution doit être mis en pause pour permettre l’exécution d’un autre processus. Ne pas attendre des heures qu’un programme planté cède la priorité.
Processus vs Thread La plupart des systèmes d’exploitation offrent la distinction entre : Processus lourd : Un programme (un ensemble d’instructions) à exécuter. Sont complètement isolés les uns des autres. Processus léger : Thread Portion de code capable de s’exécuter en parallèle à d’autres traitements. Ils partagent code, données et ressources. Mais peuvent disposer de leurs propres données.
Création de thread 2 manières pour créer un Thread : Une classe qui dérive de java.lang.Thread. java.lang.Thread implémente Runnable. Il faut redéfinir la méthode run(). Une classe qui implémente l’interface Runnable Il faut implémenter la méthode run()
Méthode 1 : Sous-classer Thread class Thread1 extends Thread { Thread1() {...} // Le constructeur ... public void run() { ... // Ici ce que fait le thread } Thread1 p1 = new Thread1(); // Création du thread p1 p1.start(); // Démarre le thread et exécute p1.run()
Méthode 2 : une classe qui implémente Runnable class Thread2 implements Runnable { Thread2() { ...} // Constructeur ... public void run() { ... // Ici ce que fait le thread } Thread2 p = new Thread2(); Thread p2 = new Thread(p); p2.start(); // Démarre le thread et exécute p.run()
Quelle solution choisir ? Méthode 1 : sous-classer Thread Lorsqu’on désire paralléliser une classe qui n’hérite pas déjà d’une autre classe (classe autonome). Attention : héritage simple. Méthode 2 : implémenter Runnable Lorsqu’une super-classe est imposée. Cas des applets public class MyThreadApplet extends Applet implements Runnable {}
Applet et thread Un thread qui conte de 1 à 20, il fait l’affichage à la fois dans l’applet et sur la console. import java.applet.*; import java.awt.*; public class CounterThread extends Applet implements Runnable { Thread t; int Count; public void init() { Count=0; t=new Thread(this); // t doit prendre un objet this comme paramètre t.start(); } public void run() { while (Count < 20) { Count++; repaint(); try { t.sleep(1000); } catch (InterruptedException e) {} } public void paint(Graphics g) { g.drawString(Integer.toString(Count),10,10); System.out.println("Count= "+Count);
ThreadGroup ThreadGroup : dans java.lang. Plusieurs Threads peuvent s’exécuter en même temps, il serait utile de pouvoir les manipuler comme une seule entité. Pour les suspendre, Pour les arrêter, ... Java offre cette possibilité via l’utilisation des groupes de threads : java.lang.ThreadGroup. On groupe un ensemble nommé de threads. Ils sont contrôlés comme une seule unité. La JVM crée au minimum un groupe de threads nommé main. Par défaut, un thread appartient au même groupe que celui qui l’a crée (son père). getThreadGroup() : pour connaitre son groupe.
Création d’un groupe de threads Pour créer un groupe de threads : ThreadGroup groupe1 = new ThreadGroup("GP1"); Thread p1 = new Thread(groupe1, "P1"); Thread p2 = new Thread(groupe1, "P2"); Thread p3 = new Thread(groupe1, "P3"); Le contrôle des ThreadGroup passe par l’utilisation des méthodes standards qui sont partagées avec Thread : Exemple : interrupt(), destroy(), Par exemple : appliquer la méthode interrupt() à un ThreadGroup revient à invoquer pour chaque Thread du groupe cette même méthode.
Création d’une arborescence de threads ThreadGroup groupe1 = new ThreadGroup("GP1"); Thread p1 = new Thread(groupe1, "P1"); Thread p2 = new Thread(groupe1, "P2"); Thread p3 = new Thread(groupe1, "P3"); ThreadGroup groupe11 = new ThreadGroup(groupe1, "GP11"); Thread p4 = new Thread(groupe11, "P4"); Thread p5 = new Thread(groupe11, "P5");
Threads démons 2 types de threads : Les threads utilisateur : L’activité de ce type de thread est limitée dans le temps, c’est à dire que son scénario est un algorithme qui se termine au bout d’un temps fini. La JVM fonctionne tant qu’il reste des threads utilisateurs en exécution. Les démons : Threads qui s’exécutent en tâche de fond tant que le programme tourne. Difficile de trouver une condition d’arrêt pour ce type de thread. Un démon “meurt” lorsque l’application se termine. La JVM s’arrête s’il ne reste plus que des démons. Les démons sont là seulement pour rendre service aux threads utilisateur. Exemple : Ramasse-miettes (DestroyJavaVM). Un thread horloge qui tourne en tâche de fond.
On peut lancer l’horloge avec la simulation de course. Threads démons La méthode void setDaemon (boolean) de la classe Thread permet d’indiquer que le thread sera un démon (thread utilisateur par défaut). Elle doit être appelée avant le démarrage du thread par l’appel de start(). Exemple : class Horloge extends Thread { public Horloge () { setDaemon (true) ; } public void run () { while (true) { try { Thread.sleep (300) ; } catch (InterruptedException e) {} System.out.println ("tip") ; } On peut lancer l’horloge avec la simulation de course.
Synchronisation des threads Premier type de synchronisation : Synchronisation compétitive : lorsque plusieurs threads utilisent la même ressource (exemple : imprimeur, l’accès concurrents à un compte en banque). Définir un verrou / une section en exclusion mutuelle. En Java, chaque objet possède un moniteur (superviseur) qui peut garder les sections critiques. Il assure que seul un thread à la fois peut exécuter la section critique qu’il supervise. synchronized (objet) { instr } moniteur associé à objet. synchronized void Methode(){....} moniteur associé à la méthode. L’objet est verrouillé pendant que le bloc synchronisé est exécuté. synchronized (objet) T1 est entrain d’exécuter le bloc synchronisé T2 T3 T4 En Java, chaque objet possède une queue d’attente Queue d’attente
Moniteur associe à un objet 1 seul moniteur
Moniteurs associent à 2 objets Le thread qui exécute synchronized d’un objet devient propriétaire du moniteur de cet objet
Synchronisation des threads Deuxième type de synchronisation : Synchronisation coopérative : lorsqu’un thread attend la fin de l’exécution d’un autre avant de poursuivre son exécution. Priorités entre threads : méthode setPriority(entier). Les priorités des threads se situent dans un intervalle de 1 à 10. Trois constantes représentent les valeurs limites et médianes. Plus la valeur de la priorité est élevée, plus grande sera la priorité du thread pour l'accès au processeur. MIN_PRIORITY : priorité minimum (1) NORM_PRIORITY : priorité normale (5) MAX_PRIORITY : priorité maximum (10) La priorité normale est affectée par défaut à un nouveau thread. Coureur A = new Coureur("A"); Coureur B = new Coureur("B"); A.setPriority(Thread.MAX_PRIORITY); B.setPriority(Thread.MIN_PRIORITY);
Synchronisation des threads Méthode join() : synchroniser deux écrivains ecrivainA = new Ecrivain ("ABC"); ecrivainB = new Ecrivain ("XYZ"); ecrivainA.start(); try { ecrivainA.join(); } catch(InterruptedException e) {} ecrivainB.start(); Autre type de coopération avec : wait, notify, notifyAll. Sémaphores.
Exemple de coopération des threads (Professeur vs élèves) Un professeur qui apprends à des élèves des nouveaux mots. Les élèves doivent répéter chaque mot. Parler Maison Bonjour Discuter .. . Parler Maison Thread principal 2 Threads : 2 élèves
Problème de coopération des threads public class EcoleDesPerroquets14 { static String mot = null; public static void main(String[] args) { Perroquet14 perroquet1 = new Perroquet14("coco"); perroquet1.start(); Perroquet14 perroquet2 = new Perroquet14("jaco"); perroquet2.start(); String reponse = null; do { mot = reponse; System.out.println ("nouveau mot pour perroquet ? (sinon non)"); reponse = Saisie.litexte(); } while (! reponse.equals("non")); System.exit(1); class Perroquet14 extends Thread { private String nom; public Perroquet14(String n) { super(n); nom = n; } public void repeter() { System.out.println(nom + " "+ EcoleDesPerroquets14.mot); } public void run() { while (true) { while (EcoleDesPerroquets14.mot == null) try {Thread.sleep(2000);} catch (Exception e) { } for (int n=0; n<3; n++) repeter(); try{Thread.sleep((int)(Math.random()*3000)); } catch(InterruptedException e) { }
Problème de coopération des threads Exécution: nouveau mot pour perroquet ? (sinon non) bla coco bla jaco bla blu coco blu blo jaco blo ..... Une variable static "mot" est partagée entre les 2 threads perroquets qui doivent l'apprendre puis le répéter et le thread main qui en saisit un nouveau. L'ensemble n'est pas du tout synchronisée : les perroquets ne savent pas si le mot est un nouveau à apprendre ou si c'est l'ancien. Au début, ils doivent attendre avec une boucle pour le premier mot et si le user/professeur fournit trop rapidement des nouveaux mots, les 2 perroquets peuvent "en rater" ! Le but : Il faudrait avoir un mécanisme d'attente entre le professeur (user) et les élèves perroquets : les élèves attendent un nouveau mot à apprendre, le professeur, quand il enseigne un nouveau mot, devrait attendre que les élèves aient le temps de l'apprendre et le répéter, avant d'en enseigner un nouveau.
Coopération avec : wait et notifyAll public class EcoleDesPerroquets15 { public static void main(String[] args) { AuTableau autableau = new AuTableau(); Perroquet15 perroquet1 = new Perroquet15("coco", autableau); perroquet1.start(); Perroquet15 perroquet2 = new Perroquet15("jaco", autableau); perroquet2.start(); String reponse = "bonjour"; do {autableau.enseigner(reponse); System.out.println("nouveau mot pour perroquet ? (sinon non)"); reponse = Saisie.litexte(); } while (! reponse.equals("non")); System.exit(1); }} class Perroquet15 extends Thread { private String cri; private String nom; private AuTableau autableau; public Perroquet15(String n, AuTableau a) { super(n); nom = n; autableau = a ; cri = ""; } Coopération avec : wait et notifyAll public void repeter() { System.out.println(nom + " "+ cri); } public void run() { while (true) { cri = autableau.apprendre(); for (int n=0; n<3; n++) repeter(); } class AuTableau { private String motAapprendre = null; synchronized String apprendre() { try { wait(); } catch (Exception e) {} return motAapprendre; } synchronized void enseigner (String mot) { motAapprendre = mot ; notifyAll(); Utiliser par les élèves Utiliser par le professeur
Coopération avec : wait et notifyAll Exécution: nouveau mot pour perroquet ? (sinon non) miam coco miam jaco miam encore coco encore jaco encore non
Coopération avec : wait et notifyAll Les trois méthodes (wait, notify et notifyAll) sont définies dans la classe java.lang.Object et sont donc héritées par toute classe. La méthode wait suspend (bloque) l'exécution d'un thread, en attendant qu'une certaine condition soit réalisée. La réalisation de cette condition est signalée par un autre thread par la méthode notify ou notifyAll. Lorsque la méthode wait est invoquée à partir d'une méthode synchronized, en même temps que l'exécution est suspendue, le verrou posé sur l'objet par lequel la méthode a été invoquée est relâché. Dès que la condition de réveil survient, le thread attend de pouvoir reprendre le verrou et continuer l'exécution. Une autre version de wait prend en argument un entier de type long qui définit la durée d’attente maximale (en millisecondes). Si ce temps est dépassé, le thread est réveillé. Différence entre sleep et wait : sleep bloque l’exécution mais le thread garde le verrou (termine son exécution). wait bloque l’exécution mais le thread libère le verrou (un deuxième thread peut exécuter la section critique).
Coopération avec : wait et notifyAll
Coopération avec : wait et notifyAll Désormais, les perroquets attendent (wait) que le user/professeur leurs enseigne un nouveau mot et le note au tableau. Dès qu'il a noté le mot au tableau, le professeur indique (notifyAll) aux perroquets en attente qu’ils peuvent le lire et le répéter. La méthode wait() d'un objet doit se trouver dans un bloc "synchronized" sur ce même objet, elle doit acquérir le verrou de l’objet. synchronized (objet) { ... objet.wait(); ...} Indique au moniteur de l’objet qu'elle se met en attente (comme sleep), mais (contrairement à sleep), elle libère le verrou sur l’objet, elle termine la phase de section critique. Néanmoins, quand le thead sera réveillé/débloqué, si plusieurs thread attendaient, un seul à la fois exécutera le reste du bloc en section critique.
Coopération avec : wait et notifyAll La méthode notifyAll() d'un objet doit se trouver dans un bloc "synchronized" sur ce même objet, elle doit acquérir le verrou de l'objet. synchronized (objet) { ... objet.notifyAll(); ...} Indique au moniteur de l’objet que tous les threads en attente (wait) sur l'objet doivent être réveillés.
notify public void repeter() { System.out.println(nom + " "+ cri); } public class EcoleDesPerroquets16 { public static void main(String[] args) { AuTableau autableau = new AuTableau(); Perroquet16 perroquet1 = new Perroquet16("coco", autableau); perroquet1.start(); Perroquet16 perroquet2 = new Perroquet15("jaco", autableau); perroquet2.start(); String reponse = "bonjour"; do {autableau.enseigner(reponse); System.out.println("nouveau mot pour perroquet ? (sinon non)"); reponse = Saisie.litexte(); } while (! reponse.equals("non")); System.exit(1); }} class Perroquet16 extends Thread { private String cri; private String nom; private AuTableau autableau; public Perroquet15(String n, AuTableau a) { super(n); nom = n; autableau = a ; cri = ""; } notify public void repeter() { System.out.println(nom + " "+ cri); } public void run() { while (true) { cri = autableau.apprendre(); for (int n=0; n<3; n++) repeter(); } class AuTableau { private String motAapprendre = null; synchronized String apprendre() { try {wait(); } catch (Exception e) {} return motAapprendre; } synchronized void enseigner (String mot) { motAapprendre = mot ; notify();
notify Un seul therad est libéré de l’attente par notify. Exécution: coco bonjour miam coco miam nouveau mot pour perroquet ? (sinon non) encore jaco encore bizarre coco bizarre vraiment jaco vraiment non notify Un seul therad est libéré de l’attente par notify. Un seul perroquet élève apprend à la fois. notify() ne réveille qu'un thread à la fois, il n'y a pas de spécification sur le thread choisi ! c'est le contrôleur de thread qui choisit.
Demi-synchronisation (wait et sleep) public class EcoleDesPerroquets17 { public static void main(String[] args) { AuTableau autableau = new AuTableau(); Perroquet15 perroquet1 = new Perroquet15("coco", autableau); perroquet1.start(); Perroquet15 perroquet2 = new Perroquet15("jaco", autableau); perroquet2.start(); String reponse = "bonjour"; do {autableau.enseigner(reponse); System.out.println("nouveau mot pour perroquet ? (sinon non)"); reponse = Saisie.litexte(); } while (! reponse.equals("non")); System.exit(1); }} class Perroquet17 extends Thread { private String cri; private String nom; private AuTableau autableau; public Perroquet15(String n, AuTableau a) { super(n); nom = n; autableau = a ; cri = ""; } Demi-synchronisation (wait et sleep) public void repeter() { System.out.println(nom + " "+ cri); } public void run() { while (true) {cri = autableau.apprendre(); for (int n=0; n<3; n++) { repeter(); try {Thread.sleep(2000); } catch(InterruptedException e) { } } class AuTableau { private String motAapprendre = null; synchronized String apprendre() { try { wait();} catch (Exception e) {} return motAapprendre;} synchronized void enseigner (String mot) { motAapprendre = mot ; notifyAll(); }
Demi-synchronisation (wait et sleep) Exécution: nouveau mot pour perroquet ? (sinon non) coco bonjour Miam jaco Miam 1 2 3 4 coco 1 jaco 3 non Les perroquets répètent puis se mettent en attente d’un nouveau mot à apprendre dès qu’un nouveau mot est au tableau, les perroquets en attente sont notifiés (synchronisation avec wait et notifyAll). Mais les perroquets qui ne sont pas en attente (qui ont été déjà notifiés) perdront certains mots si le professeur va trop vite (à cause du Thread.sleep(2000)). C’est une demi-synchronisation : Le professeur n’attend pas que les perroquets aient eu le temps de lire et répéter.
Simuler une station de bus Des usagers arrivent à la station pour y attendre un bus ; un bus arrive, charge tous les usagers présents dans la station puis repart. Si un usager arrive trop tard il loupe le bus. Station de bus
Utiliser par les usagers class Station { public synchronized void attendreBus () { try { wait(); } catch (Exception e) {} } public synchronized void chargerUsagers () { notifyAll () ; } class Usager extends Thread { private String nom ; private Station s ; private int heureArrivee ; public Usager (String nom, Station s, int heureArrivee) { this.nom = nom ; this.s = s ; this.heureArrivee = heureArrivee ; public void run () { try { sleep (heureArrivee) ; } catch (InterruptedException e) {} System.out.println (nom + " arrive a la station") ; s.attendreBus () ; System.out.println (nom + " est monte dans le bus") ; Utiliser par les usagers Utiliser par le bus class Bus extends Thread { private Station s ; private int heureArrivee ; public Bus (Station s, int heureArrivee) { this.s = s ; this.heureArrivee = heureArrivee ; } public void run () { try { sleep (heureArrivee) ; catch (InterruptedException e) {} System.out.println ("Bus arrive a la station") ; s.chargerUsagers () ; System.out.println ("Bus repart de la station") ;
Les 4 classes sont dans le fichier BusSimple.java class BusSimple { public static void main (String args[]) { Station Gare = new Station () ; Bus b = new Bus (Gare, 2000) ; Usager u [ ] = { new Usager ("A", Gare, 1500), new Usager ("B", Gare, 3000), new Usager ("C",Gare, 1000), new Usager ("D",Gare, 1500), new Usager ("E", Gare, 1000)} ; b.start () ; for (int i = 0 ; i < u.length ; i++) u[ i ].start () ; } Exécution : E arrive a la station C arrive a la station D arrive a la station A arrive a la station Bus arrive a la station Bus repart de la station C est monte dans le bus E est monte dans le bus D est monte dans le bus A est monte dans le bus B arrive a la station
Simuler un match de foot entre 2 joueurs 2 joueurs (2 threads) partage la même ressource qui est un ballon. Au départ, les deux joueurs sont bloqués par un wait car ils demandent le ballon. Le ballon est donné à l’un des deux joueurs (à l’aide de notify). Un seul joueur peut récupérer le ballon, il possédera le ballon pendant certain moment (avec un sleep), il peut évidemment le déplacer, le lâcher par la suite et notifier le deuxième joueur qui va faire la même chose. 2ème thread 1er thread
Problème du Producteur et du Consommateur public class EcoleDuPerroquet18 { public static void main(String[] args) { AuTableau autableau = new AuTableau(); Perroquet18 perroquet = new Perroquet18("coco", autableau); perroquet.start(); //consommateur Maitre maitre = new Maitre(autableau); maitre.start(); //producteur }} class Maitre extends Thread { private AuTableau autableau; private String reponse = null; public Maitre (AuTableau a) { autableau = a ; } public void run() { for (int n=0; n<4; n++) { System.out.println("nouveau mot a enseigner au perroquet ?"); Thread.currentThread().yield(); reponse = Saisie.litexte(); autableau.enseigner(reponse); } }} class Perroquet18 extends Thread { private String cri; private String nom; private AuTableau autableau; public Perroquet18 (String n, AuTableau a) { super(n); nom = n; autableau = a ; cri = ""; } public void repeter() { System.out.println(nom + " "+ cri); } public void run() { while (true) { cri = autableau.apprendre(); for (int n=0; n<3; n++) { repeter(); try { Thread.sleep(2000); } catch(InterruptedException e) { } }} class AuTableau { private String motAuTableau = null; synchronized String apprendre() { String motAapprendre; while (motAuTableau == null) try { wait(); } catch (Exception e) {} motAapprendre = motAuTableau ; motAuTableau = null; notify(); return motAapprendre; } synchronized void enseigner (String motNouveau) { while (motAuTableau != null) motAuTableau = motNouveau;
Problème du Producteur et du Consommateur Le problème est simplifié, un seul professeur et un seul perroquet et chacun attend l'autre : Le perroquet attend pour un nouveau mot à apprendre. Le professeur attend que le perroquet ait lu puis répété le mot. Ce pattern classique est appelé Producteur-Consommateur : Il faut synchroniser le fonctionnement de chacun afin que : Le consommateur attende quand il n’y a rien à consommer. while (motAuTableau == null) Le producteur attende quand le consommateur n'est pas prêt à consommer. while (motAuTableau != null) Producteur Consommateur 1 2 Dépose le produit En attente du produit (rien à consommer) notify () wait() Réveiller le producteur 4 wait() notify () Consommer le produit 5 En attente (Le consommateur n’est pas prêt à consommer) 3
Problème du Producteur et du Consommateur Exécution: nouveau mot a enseigner au perroquet ? mille coco mille bb coco bb non coco non
Producteurs-Consommateurs Des threads "producteur" qui produisent des données et les mettent dans une file de messages. Des threads "consommateur" qui prennent les données de la file. Les producteurs et les consommateurs ne doivent pas accéder à la file en même temps (section critique). File de messages Producteurs Consommateurs P1 P2 P3 P4 P5 C1 C2 C3
Producteurs-Consommateurs Synchronisation par rapport à la file de message. Les producteurs : Quand la file est pleine : bloquer. Quand il existe des places libres : débloquer. Les consommateurs : Quand la file est vide : bloquer. Quand il existe des données dans la file : débloquer.
Producteurs-Consommateurs Un moniteur d’accès propose des méthodes pour accéder à la file de manière contrôlée. import java.util.ArrayList; public class Moniteur { private ArrayList<Object> file;// file de messages. public Moniteur(int capacite) { … } file=new ArrayList<Object>(capacite); // créer un moniteur pour une file d'attente de capacité spécifiée public synchronized void deposer (Object o) { … } // déposer une donnée. bloquer si la file est pleine. public synchronized Object prendre() { … } // prendre une donnée. bloquer si la file est vide} // producteur while (…) { moniteur.deposer(o); } // consommateur System.out.println(moniteur.prendre()); Utiliser par les producteurs Utiliser par les consommateurs
Semaphore en JAVA Un sémaphore encapsule un entier et deux opérations atomiques d’incrémentation et de décrémentation. Avant 2005 (avant l’apparition de J2SE 5.0). class Semaphore { int compteur; Semaphore (int init) { compteur=init; } public synchronized void P(){ compteur--; if (compteur<0) try{wait();} catch (InterruptedException e){} public synchronized void V(){ compteur++; if (compteur<=0) notify(); T5 compteur = 4 T1 P() T2 T3 V() T4 Section critique Le compteur détermine le nombre max de threads qui peuvent accéder à la section critique.
Semaphore en JAVA Opération P (acquire()) : décrémente le compteur ; bloque s'il est négatif en attendant de pouvoir le décrémenter. Opération V (release()) : incrémente le compteur. Fait un notify si le compteur est négatif ou nul. On peut voir le sémaphore comme un ensemble de jetons, avec deux opérations : Prendre un jeton, en attendant si nécessaire qu'il y en ait ; acquire(). Probablement avec wait. Déposer un jeton. release(). Probablement avec notify. Un sémaphore à 1 jeton est très similaire à un verrou. La version 1.5 de Java et son package java.util.concurrent (ainsi que ses sous-packages) fournissent des outils de synchronisation de plus haut niveau.
java.util.concurrent.Semaphore import java.util.concurrent.Semaphore; class Process2 extends Thread{ private int id; private Semaphore sem; public Process2(int i, Semaphore s) { id = i; sem = s; } private void busy() { try{sleep((int)(Math.random()*1000));} catch (InterruptedException e){} } private void noncritical() { System.out.println("Thread " + id + " n'est pas dans la section critique"); busy(); } private void critical() { System.out.println("Thread " + id + " entre dans la section critique"); busy(); System.out.println("Thread " + id + " sort de la section critique"); public void run() { noncritical(); try { sem.acquire(); } catch (InterruptedException e){ } critical(); sem.release(); } public static void main(String[] args) { Semaphore sem = new Semaphore(1); Process2[] p = new Process2[4]; for (int i = 0; i < 4; i++) { p[i] = new Process2(i, sem); p[i].start();
Semaphore en JAVA Exécution : Semaphore sem = new Semaphore(1); Thread 0 n'est pas dans la section critique Thread 3 n'est pas dans la section critique Thread 2 n'est pas dans la section critique Thread 1 n'est pas dans la section critique Thread 3 entre dans la section critique Thread 3 sort de la section critique Thread 2 entre dans la section critique Thread 2 sort de la section critique Thread 1 entre dans la section critique Thread 1 sort de la section critique Thread 0 entre dans la section critique Thread 0 sort de la section critique
Semaphore en JAVA Exécution : Semaphore sem = new Semaphore(4); Thread 0 n'est pas dans la section critique Thread 1 n'est pas dans la section critique Thread 3 n'est pas dans la section critique Thread 2 n'est pas dans la section critique Thread 0 entre dans la section critique Thread 2 entre dans la section critique Thread 3 entre dans la section critique Thread 1 entre dans la section critique Thread 2 sort de la section critique Thread 3 sort de la section critique Thread 0 sort de la section critique Thread 1 sort de la section critique
Application des sémaphores Gestion des entrées/sorties d’un parking Un parking de capacité N places. Gestion d’une salle d’attente. Chez un médecin, coiffeur, assurance, … M chaises disponibles.
Exercice : Coiffeur dormeur (producteur/consommateur avec sémaphore) Un coiffeur possède un salon avec un siège de coiffeur et une salle d’attente comportant un nombre fixe N de fauteuils. S’il n’y a pas de client, le coiffeur se repose sur son siège de coiffeur. Quand un client arrive : Si la salle d’attente est pleine, le client repasse plus tard. S’il trouve le coiffeur endormi, il le réveille, s’assoit sur le siège de coiffeur et attend la fin de sa coupe de cheveux. Si le coiffeur est occupé lorsque le client arrive, le client s’assoit et s’endort sur une des N chaises de la salle d’attente, il doit attendre que le siège de coiffeur soit libre. Lorsque le coiffeur a terminé une coupe de cheveux, il fait sortir son client courant et va réveiller un des clients de la salle d’attente. Si la salle d’attente est vide, le coiffeur se rendort sur son siège jusqu’`a ce qu’un nouveau client arrive. Le but de cet exercice est d’associer un thread au coiffeur ainsi qu’`a chaque client et de programmer une séance de coiffeur dormeur en Java. L’utilisation des sémaphores est demandée pour garantir l’exclusion mutuelle parmi les threads.
Exercice : Coiffeur dormeur (producteur/consommateur avec sémaphore) Clients Salle d’attente Coiffeur ChaisesLibres.acquire() ChaisesLibres.release() Reveiler.acquire() Reveiler.release() 1 2 ChaiseCoiffeur.acquire() 3 ChaiseCoiffeur.release() 5 4 Nom ChaisesLibres Reveiler ChaiseCoiffeur Semaphore ChaisesLibres = new Semaphore (nbChaises); Semaphore Reveiler = new Semaphore(0); // pas de jeton, donc il faut qu’un release devance le acquire Semaphore ChaiseCoiffeur = new Semaphore(0);
Interblocage public class Interblocage { public static void main(String[] args) { final int[] tab1 = { 1, 2, 3, 4 }; final int[] tab2 = { 0, 1, 0, 1 }; final int[] tabAdd = new int[4]; final int[] tabSub = new int[4]; Thread tacheAdd = new Thread() { public void run() { synchronized(tab1) { System.out.println ("Thread tacheAdd lock tab1"); travailHarassant(); synchronized(tab2) { ("Thread tacheAdd lock tab2"); for (int i=0; i<4 ; i++) tabAdd[i] = tab1[i] + tab2[i]; } }; Thread tacheSub = new Thread() { public void run() { synchronized(tab2) { System.out.println ("Thread tacheSub lock tab2"); travailHarassant(); synchronized(tab1) { ("Thread tacheSub lock tab1"); for (int i=0; i<4 ; i++) tabAdd[i] = tab1[i] - tab2[i]; } } } }; tacheAdd.start(); tacheSub.start(); } static void travailHarassant() { try{Thread.sleep((int)(Math.random()*50+25));} catch (InterruptedException e) {} } La classe interne anonyme utilise le mot clé new suivi d'un nom de classe ou interface que la classe interne va respectivement étendre ou implémenter
Interblocage Supposons que tab1 et tab2 sont susceptibles d'être modifiés par d'autres threads qui effectueraient ces modifications en acquérant leur verrou. Pour éviter une modification au cours de leur calcul, les 2 threads cherchent à obtenir les verrous de tab1 et tab2. Le problème d'interblocage est que : La tacheAdd a obtenu le lock de tab1 et attend pour obtenir celui de tab2, puis ils les libérera tous les 2. La tacheSub a obtenu le lock de tab2 et attend pour obtenir celui de tab1, puis ils les libérera tous les 2. Ils sont donc bloqués, c'est un Deadlock. Un interblocage de tâches parallèles qui ont acquis le verrou de certaines ressources et cherchent à en obtenir d'autres, le graphe des verrouillages est insastifaisable, comme les tâches ne peuvent revenir en arrière (cad libérer des ressources), la situation est définitivement bloquée. Exécution : Thread tacheAdd lock tab1 Thread tacheSub lock tab2 <ctrl-C>