Vous êtes sur la page 1sur 79

1

Chapitre

Synchronisation entre les threads


Principe de la programmation concurrente (Rappel)
2

● Les différents threads ont des piles et des registres propres à eux, mais accèdent à
un même heap
● Trois contextes de gestion d'objets/variables :
– Ceux qui sont consultés/modifiés par 1 seul thread
● Pas de problème, même s'ils résident dans la mémoire centrale
– Ceux qui sont gérés localement au niveau d’un thread
● Class X implements Runnable : l’utilisation de la même cible pour créer
des threads nécessite l’utilisation des classes ThreadLocal,
InheritableThreadLocal pour rendre certains attributs locaux.
● Class X extends Thread : pour les attributs locaux pas de problème
d'accès concurrent
– Ceux qui sont accédés en concurrence
Attributs locaux à un thread
3

Class X implements Runnable


Problème !!
public class MonTraitementSansTL implements Runnable {
String threadName;
public void run() {
threadName = Thread.currentThread().getName();
Thread-0
System.out.println(threadName);
Thread-0
}
}
public class Test {
public static void main(String[] args) {
MonTraitementSansTL monTraitementSansTL = new MonTraitementSansTL();
Thread thread1 = new Thread(monTraitementSansTL);
Thread thread2 = new Thread(monTraitementSansTL);
thread1.start();
thread2.start();
}
}
Attributs locaux à un thread
4

Class X implements Runnable

● Si plusieurs threads exécutent le même objet cible (de type Runnable), on peut
disposer des attributs locaux propres à chaque thread.
● ThreadLocal<T> et InheritableThreadLocal<T> permettent de simuler ce
comportement
– objet déclaré comme un attribut dans l'objet Runnable
– existence d'une valeur encapsulée (sous-type de Object) propre à
chaque thread, accessible via les méthodes get() et set()
La classe ThreadLocal
5

• Il peut être pratique de stocker une donnée qui soit contextuelle à un thread : c'est
le rôle de la classe ThreadLocal. Elle a été ajoutée à Java depuis la version 1.2.
• L'utilisation d'un ThreadLocal peut avoir plusieurs objectifs :

▪ pouvoir utiliser des classes non thread-safe dans un contexte


multithread en fournissant sa propre instance à chaque thread. Ceci

permet d'éviter la synchronisation de l'accès à une unique instance

partagée par plusieurs threads (exemple : TreeSet)

• A partir de Java 5, la classe ThreadLocal est générique


La classe ThreadLocal
6

public class MonTraitementAvecTL implements Runnable {


private ThreadLocal<String> threadNameLocal = new
ThreadLocal<String>();
public void run() {
System.out.println("Mon traitement " +Thread.currentThread().getName()
+ " monThreadLocal=" + threadNameLocal);
threadNameLocal.set(Thread.currentThread().getName());
String maValeur = threadNameLocal.get();
System.out.println("Valeur = " + maValeur);
}
}
La classe ThreadLocal
7

public class TestThreadLocal {


public static void main(String[] args) {
MonTraitementAvecTL monTraitementAvecTL = new MonTraitementAvecTL();
Thread thread1 = new Thread(monTraitementAvecTL);
Thread thread2 = new Thread(monTraitementAvecTL);
thread1.start();
thread2.start();
}

Mon traitement Thread-1 monThreadLocal=java.lang.ThreadLocal@39bf53d4


Mon traitement Thread-0 monThreadLocal=java.lang.ThreadLocal@39bf53d4
Valeur = Thread-1
Valeur = Thread-0
Exemple
8
import java.util.Random;
public class CodeWithLocal implements Runnable {
public ThreadLocal<Integer> local = new ThreadLocal<Integer>(); attribut
public Object shared; spécifique à
public void run() {
chaque
String name = Thread.currentThread().getName();
Random rand = new Random();
thread
local.set(rand.nextInt(100));
System.out.println(name + ": locale: " + local.get()); attribut
shared = rand.nextInt(100); partagé entre
System.out.println(name + ": partagée: " + shared); les threads
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(name + ": après attente, locale: " + local.get());
System.out.println(name + ": après attente, partagée: " + shared);
}
}
Exemple (suite)
9

public class ExecCodeWithLocal {


public static void main(String[] args) throws InterruptedException {
// Création d'un objet code exécutable (cible)
CodeWithLocal code = new CodeWithLocal();
// Création de deux processus légers ayant ce même objet pour cible
Thread t1 = new Thread(code);
Thread t2 = new Thread(code);
// Démarrage des processus légers
t1.start();
t2.start();
// Affichage des champs de l'objet cible, après la fin des exécutions, depuis le processus
//léger initial
t1.join(); t2.join();
System.out.println("Initial: locale: " + code.local.get());
System.out.println("Initial: partagée:" + code.shared);
}
}
Exemple (suite)
10

Thread-1: locale: 17
Thread-0: locale: 21
Thread-1: partagée: 7
Thread-0: partagée: 9
Thread-1: après attente, locale: 17
Thread-1: après attente, partagée: 9
Thread-0: après attente, locale: 21
Thread-0: après attente, partagée: 9
Initial: locale: null
Initial: partagée:9
Transmission des attributs locaux
11

● La classe InheritableThreadLocal<T> hérite de la classe


ThreadLocal<T>. Elle ne possède que le constructeur par défaut.
● Un thread peut lancer un autre thread : dans ce cas, le nouveau thread
possède sa propre valeur du ThreadLocal qui sera vide par défaut.
● La classe InheritableThreadLocal<T> permet de transmettre un attribut
local d’un thread t1 à un thread t2 « fils », c'est-à-dire créé à partir de t1 (de
la même façon qu’il récupère sa priorité, son groupe, etc.).
– initialisation des attributs de classe InheritableThreadLocal<T> du
thread « fils » à partir des valeurs des attributs du thread «père», par un
appel implicite à la méthode T childValue(T).
– possibilité de redéfinir cette méthode dans une sous-classe de
InheritableThreadLocal<T>.
Transmission des attributs locaux
12

public class TestInheritableThreadLocal {


public static void main(String[] args) throws InterruptedException {
final ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() {
protected String childValue(String parentValue)
{ return parentValue + " fils"; } };

threadLocal.set("valeur");
afficherValeur(threadLocal);

Thread t = new Thread() { public void run() { afficherValeur(threadLocal);} };

t.start(); t.join();
}
private static void afficherValeur(final ThreadLocal<String> threadLocal) {
System.out.println(Thread.currentThread().getName() + " : " +
threadLocal.get()); }
} main : valeur
Thread-0 : valeur fils
Problèmes de la concurrence
13

● Le programmeur n'a pas la main sur l'architecture des processeurs, ni sur le


scheduler de l'OS.
– Problèmes pour un thread qui regarde des attributs modifiés par un autre thread
(visibilité)
– Le scheduler est préemptif
● Il arrête les thread où il veut (opération atomique)
● Les instructions des différents threads peuvent, a priori, être exécutées dans
n'importe quel ordre.
– indépendance des traitements
– organiser les threads entre-eux : moniteur, sémaphore, …
● Nécessité d'assurer la cohérence des données
– En imposant un contrôle sur les lectures/écriture
– Peut provoquer des famines ou des inter-blocages
Problèmes de la concurrence - Interblocage
14

Situation dans laquelle deux processus ou tâches P1 et P2 demandent


l’accès à deux ressources R1 et R2
Problèmes de la concurrence – Famine
15

• La famine est le problème qui se produit lorsque des processus à haute priorité
continuent à s'exécuter et que les processus à faible priorité sont bloqués pour
une durée indéterminée.
• En cas de famine, les ressources demandées sont continuellement utilisées par
des processus hautement prioritaires.
Registres et réordonancement
16

● Deux instructions d'un même thread doivent respecter leur séquencement s’ils
dépendent l’une de l'autre
a=3;
c=2;
b=a; // a=3 doit être exécuté avant b=a
– Mais on ne sait pas si c=2 s'exécute avant ou après b=a
(réordonnancement, optimisation)
● Deux instructions de deux processus légers distincts n'ont pas a priori d'ordre
d'exécution à respecter.
Registres et réordonancement
17

public class A implements Runnable { public class ATest {


public boolean start; // false
public int value; public void doSomething(A a) {
a. value = 3;
public void run() {
a.start = true;
while(!this.start) ; // attente active f(a. value);
System.out.println(value); // affiche 0 }
}
} public void f(int ent) {
System.out.println(ent);
}

public static void main (String[] args) {


Runnable cible=new A();
Thread t=new Thread(cible);
t.start();
ATest at=new ATest();
at.doSomething((A) cible);
}
}
Registres et réordonancement
18

public class A implements Runnable {


public boolean start; // false
public int value;
public void run() {
while(!this.start) ; // attente active
System.out.println(value); // affiche 0
}
}

public void doSomething(A a) {


a.value = 3;
a.start=true; a.start=true;
f(a.value); a.value = 3;
} Réordonnancement f(a.value);
Problèmes liés au processeur
19

● Le JIT
– stocke les variables dans des registres pour éviter les aller-retour à la
mémoire centrale
– réordonnance les instructions à l'intérieur d'un thread pour de meilleures
performances (pipeline)

● Les long et double sont chargés en 2 fois sur une machine 32bit

– Problème pour un autre thread qui accède aux mêmes variables :


cohérence
Problèmes liés au processeur
20

Avec un pipeline, le processeur peut commencer à exécuter une nouvelle instruction


sans attendre que la précédente soit terminée. Chacune des étapes d’un pipeline est
appelé étage. Le nombre d'étages d'un pipeline est appelé sa profondeur.

Exemple avec un pipeline à 5 étages :


1) Lecture de l’instruction (IF) 2) Décodage de l’instruction (ID) 3) Exécution de
l’instruction (EX) 4) Accès mémoire (MEM) 5) Ecriture du résultat (WB)
Visibilité et mise à jour des variables
21

– Chaque processus léger stocke les valeurs qu'il utilise dans ses registres : la mise
à jour n'est pas systématique
Les registres
22

● Deux threads :
– Le premier exécute tant qu'une valeur est false
public class A implements Runnable { Ici, la valeur false est mise
public boolean start; // false dans un registre
public void run() {
while(!this.start) ; // attente active
...
}
}
– Au bout d'un temps, l'autre thread change la valeur
public void doSomething(A a) {
// ... Ici, la valeur true est mise en mémoire,
a.start=true; mais le premier thread ne recharge
} pas cette valeur dans son registre
Solution : le mot clé volatile
23

● Utilisé comme un modificateur d’attribut


● Assure la cohérence entre la mémoire de travail et la mémoire principale (pas de
cache possible).
– La mise à jour est forcée à chaque lecture/écriture pour prendre en compte les
dernières modifications
● L'écriture d'une variable volatile force la sauvegarde de tous les registres dans
la mémoire
● Interdit le réordonnancement des instructions contenant des attributs volatiles et
les autres
● Assure l'atomicité de la lecture et de l'écriture des variables de type double et
long
Visibilité et mise à jour des variables
24

Problème engendré par la mémoire cache :


Solution : le mot clé volatile
25

● Deux threads :
– Le premier exécute tant qu'une valeur est false
public class A implements Runnable {
public volatile boolean start; // false
public void run() {
while(!this.start) ; // attente active
...
}
}
– Au bout d'un temps, l'autre thread change la valeur
public void doSomething(A a) {
// ...
a.start=true;
}
le mot clé volatile
26

• les variables volatiles sont stockées dans le tas (Heap)


• le mot clé volatile ne peut pas être utilisée avec une variable locale (erreur de
compilation)
• Attention (1) : dans le cas d'un objet de référence volatile, il est garanti que la
référence elle-même sera visible par les autres threads en temps opportun,
mais il n'en va pas de même pour ses variables membres. Il n'y a aucune
garantie que les données contenues dans l'objet seront visibles de manière
cohérente en cas d'accès concurrentiel.
Le mot-clef volatile
27

● Attention (2) : Lors de l'utilisation du mot clé volatile sur un tableau, cela définit une
référence volatile sur un tableau et non pas une référence sur un tableau de variables
volatiles. Si le mot clé volatile est utilisé sur un tableau, c'est la lecture et la modification
de la référence à ce tableau qui est volatile. Lors de la lecture d'un élément du tableau, la
lecture de la référence du tableau est volatile mais pas la lecture de la valeur de l'élément
concerné. Lors de la modification d'un élément du tableau, la lecture de la référence du
tableau est volatile mais pas la modification de la valeur de l'élément. Il n'est pas possible
de déclarer volatile les éléments d'un tableau. Ainsi, la modification de la valeur d'un
élément du tableau n'a pas les garanties offertes par le mot clé volatile.
Le langage ne permet pas d'avoir les garanties offertes par le mot clé volatile sur les
éléments d'un tableau. Pour obtenir ces garanties, il faut utiliser les classes
AtomicIntegerArray, AtomicLongArray et AtomicReferenceArray du package
java.util.concurrent.atomic. Elles offrent une sémantique similaire à volatile lors des
opérations de lecture/écriture sur les éléments du tableau.
Le mot-clef volatile
28

● Attention (3) : n'assure pas l'atomicité des opérations composites


– Y compris l'incrémentation ou la décrémentation.
– Les instructions composant ces opérations peuvent s'entrelacer entre
plusieurs threads concurrents
▪ Exemple :
1) Un attribut x d'un objet o, déclaré volatile, vaut 5;
2) Deux threads concurrents font o.x++; sur o;
3) Il peut arriver que, une fois les 2 threads exécutées, (o.x == 6) soit
vrai! (et non 7 comme attendu)
Encore un problème
29
public class StayHere implements Runnable{
public class Point { private Point p;
private int x; private int val; // coordonées
private int y; public StayHere(Point p,int v){
public void change(int x,int y) { this.p = p;
this.x = x; // Scheduling ici this.val = v; }
this.y = y; public void run() {
} for(;;) {
public String toString() { p.change(val,val);
return "("+x+","+y+")"; System.out.println(p); }
} }
} public static void main(String[] args) {
Point point = new Point();
Runnable cible1 = new StayHere(point,1);
Runnable cible2 = new StayHere(point,2);
Thread t1 = new Thread(cible1);
Thread t2 = new Thread(cible2);
t1.start();
t2.start();
// peut afficher (1,2) ou (2,1)
}}
Solution : Exclusion Mutuelle
30

● Écriture/lecture entrelacées provoquent des états incohérents de la mémoire

● Volatile ne peut pas résoudre le problème de l'exemple précédent

● Impossible d'assurer qu'un thread ne « perdra » pas le processeur entre deux

instructions atomiques

➔ On peut exclure mutuellement plusieurs threads, grâce à la notion de moniteur


Les moniteurs
31

● N'importe quel objet (classe Object) peut jouer le rôle d’un moniteur.
– Lorsqu'un thread « prend » le moniteur associé à un objet, aucun autre thread
ne peut prendre ce moniteur.
– Idée : protéger les portions de code « sensibles » de plusieurs threads par le
même moniteur (section critique)
▪ Si le thread « perd » l'accès au processeur, il ne perd pas le moniteur
– un autre thread ayant besoin du même moniteur ne pourra pas
exécuter le code que ce dernier protège.
– Le mot-clef est synchronized
Les moniteurs - synchronized
32

● on protège une section critique avec objet public class Point {


moniteur
private int x;
synchronized (monitor) {
private int y;
// bloc protégé par monitor
} private final Object m
● on protège toute une méthode avec un moniteur = new Object();
associé à this void change(int x,int y) {
synchronized (m) {
public synchronized void change(int x, int y) {
this.x = x;
this.x = x;
this.y = y; this.y = y;
} }
}
public String toString() {
return "("+x+","+y+")";
}
}
Les moniteurs
33
● Attention :
▪ Il n'est pas possible d'utiliser le mot clé public class X
synchronized sur : {
▪ des variables (types primitifs)
private Object o;

▪ prototype d’un constructeur public void setO(Object o)


● Il est préférable d'utiliser le mot clé {
synchronized sur les portions de code
this.o = o;
}
critiques plutôt que sur toutes les méthodes.
● Il ne faut pas utiliser une instance qui ne soit public void x()
pas déclarée final comme moniteur pour un bloc
{
synchronized (o) //Problem
de code synchronized car il se pourrait que la // synchronization on a non-final field
référence à cet objet soit modifiée et qu'ainsi {
deux threads puissent exécuter la portion de code
}
}
en concurrence puisque le verrou ne serait pas }
posé sur le même moniteur.
Tester la possession d'un moniteur
34

• La méthode Thread.holdsLock(Object moniteur) permet de savoir si le


thread courant possède ou non un moniteur

• La méthode statique holdsLock() renvoie true si et seulement si le thread


actuel détient le moniteur sur l'objet spécifié
public class SynchronizedStack<E> {
...
public boolean add(E element) {
synchronized(array) {
if (isFull())
return false;
array[top++]=element;
}
return true;
}
private boolean isFull() {
assert Thread.holdsLock(array);
return top==array.length;
}
private int top;
private final E[] array;
}
Atomicité et Incrémentation
35

● Problème car l'incrémentation et l'assignation sont


deux opérations différentes
Scheduling entre la
public class IDGenerator { lecture et l'écriture
private volatile long counter=0;
public long getId() {
return counter++; // <==> counter=counter+1;
}
}
● On peut utiliser synchronized mais il y a mieux

public class IDGenerator {


private final Object m = new Object();
public volatile long counter=0;
public long getId() {
synchronized(m) {
return counter++;
}
}
}
java.util.concurrent.atomic
36

● Opérations atomiques sur différents types de variable : étend la notion de volatile


– Pour chacune des différentes classes AtomicBoolean, AtomicInteger,
AtomicLong, AtomicReference, …
● Autorise des implémentations plus efficaces en fonction des possibilités offertes
par les processeurs
● Opérations atomiques inconditionnelles :
– get() : lecture d'un champ volatile
– set() : écriture d'un champ volatile
– getAndSet() : lecture et écriture d’un champ volatile
● Comme getAndIncrement(), getAndAdd()
java.util.concurrent.atomic
37

public class MonCompteur {


private int valeur;
public int getValeur() { return valeur; }
public int getNextValeur() { return ++valeur; }
}
java.util.concurrent.atomic
38

public class TestMonCompteur {


public static void main(String[] args) throws InterruptedException {
final MonCompteur compteur = new MonCompteur();
Thread[] threads = new Thread[20];

Runnable thread = new Runnable() {


public void run() {

for (int i = 0; i < 10000; i++) { compteur.getNextValeur(); } } };


for (int i = 0; i < 20; i++) {
threads[i] = new Thread(thread);
37014
threads[i].start(); }
39698
for (int i = 0; i < 20; i++) { threads[i].join(); } 36682
System.out.println(compteur.getValeur()); } }
java.util.concurrent.atomic
39

public class MonCompteur {


private int valeur;
public int get() 200000
{ return valeur; }
public synchronized int incrementer()
{ return ++valeur; }
}

L'utilisation de verrous par un moniteur est une opération coûteuse et surtout


bloquante. Si la section critique est petite le surcoût est important si elle est
invoquée de nombreuses fois par plusieurs threads.
java.util.concurrent.atomic
40

import java.util.concurrent.atomic.AtomicInteger;
public class MonCompteur {
private AtomicInteger valeur = new AtomicInteger(0);
public int get() {
return valeur.get(); }
public int incrementer() {
return valeur.incrementAndGet(); }
}

200000
Opérations atomiques conditionnelles
41

● boolean compareAndSet(expectedValue, updateValue)


– Affecte atomiquement la valeur updateValue dans l'objet atomique si la
valeur actuelle de cet objet est égale à la valeur expectedValue.
● Retourne true si l'affectation a eu lieu, false si la valeur de l'objet
atomique est différente de expectedValue au moment de l'appel.
classes atomiques
42
Exemple : Liste récursive
43

▪ Imaginons une liste récursive avec une structure constante (seules les valeurs
contenues peuvent changer)
▪ Imaginons plusieurs threads qui parcourent, consultent et modifient les valeurs
d'une même liste récursive
▪ On souhaite écrire une méthode calculant la somme des éléments de la liste.
Une implantation possible
44
public class RecList {
private double value;
private final RecList next; // structure constante
private final Object m = new Object(); // protection accès à value
public RecList(double value, RecList next){
this.value = value; this.next = next;
}
public void setValue(double value) {
synchronized (m) { this.value = value; }
}
public double getValue() { synchronized (m) { return value; } }

public RecList getNext() { return next; }

public double sum() {


double sum = getValue();
RecList list = getNext();
if (list!=null)
sum += list.sum();
return sum; }
}
Mais volatile peut suffire ?
45
public class RecList {
private volatile double value;
private final RecList next; // structure constante

public RecList(double value, RecList next){


this.value = value;
this.next = next;
}
public void setValue(double value) { this.value = value; }

public double getValue() { return value; }

public RecList getNext() { return next; }


public double sum() {
double sum = getValue();
RecList list = getNext();
if (list!=null)
sum += list.sum();
return sum;
}
}
Problème dans les deux cas
46
La valeur retournée par la méthode sum() peut correspondre à une liste qui n'a jamais
existé.
Solutions?
47

● Protéger la méthode sum() par un moniteur propre au maillon sur lequel on fait
l'appel
– Le moniteur sur le premier maillon est pris et conservé jusqu'au retour de la
méthode et chaque appel récursif reprend le moniteur propre au nouveau
maillon. public double sum() {
synchronized (m) {
double sum = getValue();
RecList list = getNext();
if (list!=null)
sum += list.sum();
return sum;
}
}

▪ N'empêche pas une modification de la liste entre le début et la fin du calcul de la


somme.
▪ Sum est une vue " faiblement cohérent" de la liste
Sémantique différente: plus stricte
48
● Pour une vue "snapshot" de la liste
– Protéger les méthodes sur un seul et même moniteur partagé par tous les
maillons d'une même liste.
– Chaque appel récursif reprend le moniteur global de la liste : les moniteurs
sont réentrants
– Ce moniteur sert à exclure mutuellement
▪ le calcul de la somme de la liste et
▪ les modifications des valeurs de la liste
Les moniteurs sont "réentrants"
49

● Si un thread t détient un moniteur m alors si t exécute du code qui impose une


synchronisation sur le même moniteur m, alors le moniteur est pris deux fois, il
devra alors être libéré deux fois. On dit que les moniteurs sont ré-entrants.

● En revanche, si t crée et démarre un thread tBis, alors tBis ne détient pas


(n'hérite pas t) le moniteur m, mais entre en concurrence d'accès avec le thread t
(tBis devra attendre que t libère m pour espérer pouvoir le prendre)
Les moniteurs sont "réentrants"
50

public class MaClasse {


public synchronized methodeA()
{ methodeB(); }
public synchronized methodeB()
{ // traitements } }

Le mécanisme utilisant le mot clé synchronized est par nature réentrant : si une
portion de code synchronized est exécutée et qu'elle requière l'exécution d'une
autre portion de code avec le même moniteur alors le thread courant n'a pas
besoin d'acquérir de nouveau le moniteur puisqu'il le possède déjà.
Implantation snapshot
51
public class RecList2 {
private volatile double value;
private final RecList2 next; // structure constante
private final Object mList; // moniteur global de la liste
public RecList2(double value, RecList2 next) {
this.value = value; // Modification: exclusion mutuelle
this.next = next; public void setValue(double value) {
if(next==null) synchronized (mList) {
mList = new Object(); this.value = value;
else }
mList = next.mList; }
} // Consultation: excl. mut. inutile
public double sum() { public double getValue() {
double sum; return value;
synchronized (mList) { }
sum = getValue();
RecList2 list = getNext(); public RecList2 getNext() {
if (list!=null) return next;
sum += list.sum(); }
} }
return sum; }
Protection en contexte statique
52

● Comment est protégée une méthode statique?


Autrement dit, que signifie:
public class C {
public static synchronized int m(...){
...
}
}
– Le moniteur est l'objet classe (de type java.lang.Class)
autrement dit, c'est équivalent à:
public class C {
public static int m(...) {
synchronized (Class.forName("C")) {...}
}
}
Inter-blocage
53

● Si on n'utilise pas les méthodes dépréciées (stop(), suspend(), resume()),


le principal problème est l'inter-blocage (dead lock).

public class Deadlock { public void pong() {


Object m1 = new Object(); synchronized (m2) {
Object m2 = new Object(); synchronized (m1) {
public void ping() { // Code synchronisé sur
synchronized (m1) { // les deux moniteurs
synchronized (m2) { }
// Code synchronisé sur }
// les deux moniteurs }
} }
}
}
Synchronisation (rendez-vous)
54

● « Attendre » qu'un processus léger soit dans un état particulier.


– Attendre qu'un thread t soit terminée: t.join();
accepte éventuellement une durée (en milli/nanos)
– Attendre l'arrivée d'un événement particulier ou notifier l'arrivée de cet
événement : wait(); / notify(); (appartiennent à la classe Object)
● o.wait(); exécuté par un thread donnée t1
– nécessite d’avoir le moniteur o
– suspend le thread courant t1 et libère le moniteur o
– le thread suspendu attend (passivement) d'être réveillé par une notification sur
ce même moniteur
– Lorsque t1 reçoit une notification associée à o, il doit à nouveau acquérir le
moniteur associé à o afin de poursuivre son exécution.
wait()
55

● wait() peut accepter un délai d'attente en argument


– l'attente est alors bornée par ce délai,
– passé ce délai, le thread peut reprendre son exécution mais doit auparavant
reprendre le moniteur o.
● Si un autre thread interrompt l'attente: t1.interrupt();
– une exception InterruptedException est levée par wait();
Notification
56

● Une notification peut être émise par un thread t2


– par o.notify() : un seul thread en attente de notification sur o sera réveillé
(choisi arbitrairement);
– ou par o.notifyAll() : tous les threads en attente de notification sur o seront
réveillés (ils entrent alors en concurrence pour obtenir ce moniteur).
● L'exécution de o.notify() ou o.notifyAll() par t2
– requiert que t2 détienne le moniteur associé à o
– ne libère pas le moniteur (il faut attendre d'être sorti du bloc de code protégé par
un synchronized(o){...}
● Utilise la notion de moniteur
– si wait() ou notify() sans détenir le moniteur associé à o, alors l’exception
IllegalThreadStateException sera levée
Exemple
57

● Toujours faire les wait() dans une boucle


– Après le réveil et la ré-acquisition du moniteur, cela assure que la condition
requise est toujours valide !

// code des processus légers


// intéressés par les événements
// code du processus léger
synchronized (moniteur) {
// qui signale les événements
while (ok==0) {
synchronized (moniteur) {
moniteur.wait();
ok++;
}
moniteur.notifyAll();
ok--;
}
}
traitement();
sleep vs wait
58
Lorsque plusieurs threads souhaitent utiliser la même ressource un par un, la
méthode wait() doit être utilisée. Lorsqu’un thread ne veut effectuer aucune tâche, la
méthode sleep() doit être utilisée.
Y-a-t il un problème?
59

public class Stack {


LinkedList list = new LinkedList();
public synchronized void push(Object o) {
synchronized (list) {
list.addLast(o);
notify();
}
}
public synchronized Object pop() throws InterruptedException {
synchronized (list) {
if (list.isEmpty())
wait();
return list.removeLast();
}
}
}
Oui! (en plus de la double synchronisation)
60

public class Stack {


LinkedList list = new LinkedList();
public void push(Object o) {
synchronized (list) {
list.addLast(o);
list.notify();
}
}
public Object pop() throws InterruptedException {
synchronized (list) {
while(list.isEmpty())
list.wait();
return list.removeLast();
}
}
}
Le Sémaphore
61

• Un sémaphore est un objet qui contrôle l'accès à certaines ressources.


• Un thread qui souhaite accéder à cette ressource doit demander une autorisation à
ce sémaphore, qui peut accorder l'accès ou pas.
• En Java, les sémaphores possèdent un certain nombre d'autorisations, fixé à la
construction du sémaphore. Ce nombre marque la limite du nombre de threads qui
peuvent accéder simultanément aux ressources gardées par ce sémaphore.
• Moniteur vs Sémaphore :

• Moniteur : La section critique est accessible par un seul thread à la fois

• Sémaphore : La ressource est accessible par n threads à la fois (n spécifié lors de


l'initialisation de Sémaphore)
java.util.concurrent.Semaphore
62

● Permet de limiter le nombre d'accès à une


ressource partagée entre différents threads,
en comptant le nombre d'autorisations
acquises et rendues
● Semaphore sema = new
Semaphore(MAX, true)
crée un objet sémaphore équitable (true)
disposant de MAX autorisations
Sémaphore (acquisition, relâchement d'autorisation)
63

● sema.acquire()
– le thread courant t tente d'acquérir une autorisation.
● Si c'est possible, la méthode retourne et décrémente de un le nombre d'autorisations
● Sinon, t sera bloqué jusqu'à ce que :
– Soit le thread t soit interrompu (l'exception InterruptedException est levée)
– Soit un autre thread exécute un release() sur ce sémaphore. t peut alors être
débloqué s'il est le premier en attente d'autorisation (notion d'«équité»
paramétrable dans le constructeur)
● sema.release()
– incrémente le nombre d'autorisations. Si des threads sont en attente
d'autorisation, l'un d'eux est débloqué
Sémaphore vs Moniteur
64
class PrintingThread extends Thread {
private Printer printer;
public PrintingThread(String name, Printer printer) {
this.setName(name);
this.printer = printer;
}
public void run() {printer.print();}
}
public class SynchronizedVsSemaphore {
public static void main(String[] args) {
Printer printer = new Printer();
PrintingThread printer1 = new PrintingThread("Printer 1", printer);
PrintingThread printer2 = new PrintingThread("Printer 2", printer);
PrintingThread printer3 = new PrintingThread("Printer 3", printer);
PrintingThread printer4 = new PrintingThread("Printer 4", printer);
PrintingThread printer5 = new PrintingThread("Printer 5", printer);
PrintingThread printer6 = new PrintingThread("Printer 6", printer);
printer1.start(); printer2.start(); printer3.start();
printer4.start(); printer5.start(); printer6.start(); }
}
Sémaphore vs Moniteur
65

class Printer {
public synchronized void print() {
try {
System.out.println(
System.currentTimeMillis() + " | Thread " + Thread.currentThread().getName() +
" printing now.");
Thread.sleep(300);
} catch (Exception e) {e.printStackTrace();}
}
}

1543804752621 | Thread Printer 1 printing now.


1543804752925 | Thread Printer 2 printing now.
1543804753233 | Thread Printer 3 printing now.
1543804753536 | Thread Printer 4 printing now.
1543804753836 | Thread Printer 6 printing now.
1543804754136 | Thread Printer 5 printing now.
Sémaphore vs Moniteur
66
class Printer {
// Semaphore to allow 3 threads.
private Semaphore semaphore = new Semaphore(3);
public void print() {
try {
// Acquire one of 3 locks/permits if available. ELSE WAIT HERE !
semaphore.acquire();
System.out.println(
System.currentTimeMillis() + " | Thread " + Thread.currentThread().getName() + "
printing now.");
Thread.sleep(300);
// Release acquired lock/permit by this thread.
semaphore.release();
} catch (Exception e) {e.printStackTrace();}
}
1543805418613 | Thread Printer 1 printing now.
} 1543805418617 | Thread Printer 3 printing now.
1543805418613 | Thread Printer 2 printing now.
1543805418921 | Thread Printer 4 printing now.
1543805418921 | Thread Printer 6 printing now.
1543805418921 | Thread Printer 5 printing now.
Producteur - Consommateur
67

• 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)
Producteur – Consommateur – wait/notify
68

// Java program to implement solution of producer consumer problem.


import java.util.LinkedList;
public class Threadexample {
public static void main(String[] args) throws InterruptedException
{
// Object of a class that has both produce()
// and consume() methods
final PC pc = new PC();
// Create producer thread
Thread t1 = new Thread(new Runnable() {
public void run() {
try {
pc.produce();
}
catch (InterruptedException e) {e.printStackTrace();}
}});
// Create consumer thread
Thread t2 = new Thread(new Runnable() {
public void run() {
try { pc.consume(); }
catch (InterruptedException e) { e.printStackTrace();}
}});
Producteur – Consommateur – wait/notify
69
// Start both threads
t1.start();
t2.start();

// t1 finishes before t2
t1.join();
t2.join();
}
Producteur – Consommateur – wait/notify
70
// This class has a list, producer (adds items to list and consumer (removes items).
public static class PC {
// Create a list shared by producer and consumer, Size of list is 2.
LinkedList<Integer> list = new LinkedList<>();
private final Object m=new Object();
int capacity = 2;
// Function called by producer thread
public void produce() throws InterruptedException {
int value = 0;
while (true) {
synchronized (m)
{ // producer thread waits while list is full
while (list.size() == capacity)
m.wait();
System.out.println("Producer produced-"+ value);
// to insert the jobs in the list
list.add(value++);
// notifies the consumer thread that now it can start consuming
m.notifyAll();
// makes the working of program easier to understand
Thread.sleep(1000);
}
} }
Producteur – Consommateur – wait/notify
71
// Function called by consumer thread
public void consume() throws InterruptedException
{
while (true) {
synchronized (m)
{
// consumer thread waits while list is empty
while (list.size() == 0)
m.wait();
// to retrieve the ifrst job in the list
int val = list.removeFirst();
System.out.println("Consumer consumed-"+ val);
// Wake up producer thread
m.notifyAll();
// and sleep
Thread.sleep(1000);
}
}
}
}}
Producteur - Consommateur - Sémaphore
72

// Java implementation of a producer and consumer that use semaphores to control synchronization.
import java.util.concurrent.Semaphore;
class Q {
int item;
// semCon initialized with 0 permits to ensure put() executes first
static Semaphore semCon = new Semaphore(0);
static Semaphore semProd = new Semaphore(1);
// to get an item from buffer
void get() {
try {
// Before consumer can consume an item, it must acquire a permit from semCon
semCon.acquire();
}
catch (InterruptedException e) { System.out.println("InterruptedException caught"); }
// consumer consuming an item
System.out.println("Consumer consumed item : " + item);
// After consumer consumes the item, it releases semProd to notify producer
semProd.release();
}
Producteur - Consommateur - Sémaphore
73

// to put an item in buffer


void put(int item) {
try {
// Before producer can produce an item, it must acquire a permit from semProd
semProd.acquire();
}
catch (InterruptedException e) { System.out.println("InterruptedException caught"); }
// producer producing an item
this.item = item;
System.out.println("Producer produced item : " + item);
// After producer produces the item, it releases semCon to notify consumer
semCon.release();
}
}
Producteur - Consommateur - Sémaphore
74

// Producer class
class Producer implements Runnable {
Q q;
Producer(Q q)
{
this.q = q;
new Thread(this, "Producer").start();
}

public void run()


{
for (int i = 0; i < 5; i++)
// producer put items
q.put(i);
}
}
Producteur - Consommateur - Sémaphore
75

// Consumer class // Driver class


class Consumer implements Runnable { class PC {
Q q; public static void main(String args[])
Consumer(Q q) {
{ // creating buffer queue
this.q = q; Q q = new Q();
new Thread(this, "Consumer").start();
} // starting consumer thread
new Consumer(q);
public void run()
{ // starting producer thread
for (int i = 0; i < 5; i++) new Producer(q);
// consumer get items }
q.get(); }
}
}
Java.util.concurrent (depuis jdk1.5)

76
Java.util.concurrent - Collections
77

BlockingQueue est une interface utilisée pour qu'un thread produise des objets
qu'un autre thread consomme.
Producteur - Consommateur - threadsafe
78
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class ProducerConsumerWithBlockingQueue {


public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque<>(4);

Thread producerThread = new Thread() { public void run() {


try {
int value = 0;
while (true) {
blockingQueue.put(value);
System.out.println("Produced " + value);
value++;
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};};
Producteur - Consommateur * thread-safe
79

Thread consumerThread = new Thread() { public void run() {


try {
while (true) {
int value = blockingQueue.take();
System.out.println("Consume " + value);
Thread.sleep(1200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};};

producerThread.start();
consumerThread.start();

producerThread.join();
consumerThread.join();
}
}

Vous aimerez peut-être aussi