Académique Documents
Professionnel Documents
Culture Documents
Chapitre
● 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
● 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 :
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
threadLocal.set("valeur");
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
• 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
● 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
– 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
● 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
● 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
instructions atomiques
● 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
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
▪ 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; } }
● 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;
}
}
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
● 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();}
}
}
• 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
// 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
// Producer class
class Producer implements Runnable {
Q q;
Producer(Q q)
{
this.q = q;
new Thread(this, "Producer").start();
}
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;
producerThread.start();
consumerThread.start();
producerThread.join();
consumerThread.join();
}
}