1 / 77

La programmation concurrente en Java

La programmation concurrente en Java. Les Threads. Le problème (1). Java permet le multitasking Il le fait via les threads (processus légers) Divers threads partagent le même espace mémoire mais pas les registres

Jims
Download Presentation

La programmation concurrente en Java

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. La programmation concurrente en Java Les Threads

  2. Le problème (1) • Java permet le multitasking • Il le fait via les threads (processus légers) • Divers threads partagent le même espace mémoire mais pas les registres • Quand la JVM donne la main à un thread, il load ses registres et stocke ceux du thread précédent. • Exemple : thread1 thread2 compte1.virer(100, compte2); compte2.virer(100, compte1);

  3. Le problème (2) • En code exécutable, cela donne pour le premier thread : R1 = compte1.solde; R1 -= 100; compte1.solde = R1; R1 = compte2.solde; R1 += 100; compte2.solde = R1

  4. Le problème (3) • L’exécution des 2 threads pourra donner : thread 1 thread2 R1 = compte1.solde; R1 -= 100; compte1.solde = R1; R1 = compte2.solde R1 -= 100; R1 = compte2.solde; R1 += 100; compte2.solde = R1 compte2.solde = R1; R1 = compte1.solde; R1 += 100; compte1.solde = R1

  5. Le problème (4) • Après l’exécution le compte1 retrouve le même solde que précédemment.  • Par contre le compte2 voit son solde diminué de 100 !  • Il aurait fallu que le retrait et le dépôt soient des opérations atomiques.

  6. Créer un Thread (1) • 1ère méthode : • Définition : public class MonThread extends Thread { public void run() { ….. } } • Exécution : Thread thread = new MonThread(); thread.start(); • Attention : ne pas appeler run(); start() exécute run() dans un autre thread (unité d’exécution)

  7. Créer un Thread (2) • 2ème méthode : • Définition : public class MonRunnable implements Runnable { public void run() { ….. } } • Exécution : Thread thread = new Thread(new MonRunnable()); thread.start(); • Attention : ne pas appeler run() • Thread est le contrôleur et Runnable la tâche

  8. Cycle de vie • Le Thread existe depuis qu’on a appelé son constructeur • Avant d’appeler start(), on pourra procéder à des initialisation • Après qu’il ait terminé l’exécution de run(), le Thread continue à exister. On pourra en profiter pour récupérer des résultats

  9. Méthodes • static Thread currentThread(); • static void sleep(long millis); • static void yield(); // passe la main • static boolean interrupted(); // clear status • void run(); • void start(); • void interrupt(); • boolean isInterrupted(); // keep status • void join(); // thread.join() attend la fin de thread • InterruptedException // clear status

  10. Arrêter un Thread (1) • un Thread s’arrête quand il termine l’exécution de run(); • Si la décision de terminer est extérieure : 2 manières • en testant une condition • par interruption

  11. Arrêter un Thread (2) • En testant une condition : public class MonThread extends Thread { private volatile boolean fini = false; public void run() { while (! fini) { ….. } } public void terminer() { fini = true; } }

  12. Arrêter un Thread (3) • En interrompant le Thread : public class MonThread extends Thread { public void run() { while (! isInterrupted()) { ….. } } }

  13. Locking • Chaque objet possède un lock (un moniteur) • Si une méthode ou un bloc est synchronized, il faudra acquérir le lock de l’objet avant de l’exécuter • Si le lock est libre, on y va • Si le lock est acquis par un autre thread, on attend qu’il se libère • Si dans une méthode ou un block synchronized, on appelle une autre méthode synchronized, il ne faudra pas acquérir le lock une deuxième fois

  14. Méthode synchronized • public synchronized int getCompteur() { return compteur; } public synchronized increment() { compteur++; }

  15. Bloc synchronized • public void incrementAfterOneSecond() { try { Thread.sleep(1000); // pas dans le synchronized // car sleep ne perd pas le lock } catch (InterruptedException ie) { } synchronized (this) { compteur++; } }

  16. Méthode ou bloc static synchronized (1) • Une méthode static synchronized ne peut pas, bien évidemment, obtenir un lock sur this. • Elle obtient un lock sur un autre objet : celui représentant la classe dans Class. public class CompteTout { private static int cpt = 0; public static synchronized void incr() { cpt++; } ….. } acquiert une lock sur CompteTout.class

  17. Méthode ou bloc static synchronized (2) • Une méthode non static accédant à cpt devra acquérir le lock sur le même objet donc Faux : public synchronized int getCpt() { return cpt; } Correct : public int getCpt() { synchronized(CompteTout.class) { return cpt; } }

  18. volatile • Un champ peut être déclaré volatile • dans ce cas au moment d’un changement de thread, la JVM stocke un registre contenant cette variable dans la variable elle-même • le champ doit être sur 32 bits maximum • pas valable pour une adresse (tableau, objet) : ce serait l’adresse qui serait mis à jour pas ce qu’elle désigne • évite de mettre synchronized des méthodes qui ne font que des lectures ou des affectations simples (=) du champ • pas de ++ ou de += …..

  19. Deadlock (1) • Un deadlock arrive si deux ou plusieurs threads tentent d’acquérir un lock détenu par un autre qui ne pourra pas le libérer pour l’une ou l’autre raison • exemple : MonDeadlock

  20. Deadlock (2) public class MonDeadlock extends Thread { private boolean fini = false; public synchronized void run() { while (!fini) { System.out.println("je run"); try { Thread.sleep(1000); } catch (InterruptedException e) { } } } public synchronized void terminer() { fini = true; } }

  21. Deadlock (3) • Solution: public class MonThread extends Thread { private boolean fini = false; public void run() { while (!getFini()) { System.out.println("je run"); try { Thread.sleep(1000); } catch (InterruptedException e) { } } } private synchronized boolean getFini() { return fini; } public synchronized void terminer() { fini = true; } }

  22. Comment synchroniser (1) • Repérer dans vos classes les champs qui peuvent être modifiés et sont accédés par plusieurs threads. • mettre synchronized (sur this) les parties des méthodes qui accèdent à ces champs • si plusieurs champs d’un même objet sont accédés dans la même méthode se demander si l’ensemble de ces accès doit être atomique • si c’est le cas synchroniser (sur this) cet ensemble (synchroniser éventuellement la méthode)

  23. Comment synchroniser (2) • si des méthodes d’objets différents (de classes différentes) sont appelées dans une méthode, se demander si l’ensemble de ces accès doit être atomique. • si non, ne pas synchroniser les appels des méthodes sur les objets extérieurs : public class Portefeuille { public versement(double montant, Compte c) { synchronized (this) { this.contenu -= montant; } c.dépot(montant) // où dépôt est une méthode synchronized }

  24. Comment synchroniser (3) • si oui, ordonner les objets (acquérir les locks toujours dans le même ordre) public void transférer (Portefeuille p, Compte c) { synchronized(p) { synchronized(c) { c.dépot(p.getContenu()); } } } • une autre méthode qui devrait locker les deux même objets le ferait dans le même ordre • Ici c’est simple il suffit d’ordonner les classes

  25. Comment synchroniser (4) • si des champs d’objets différents (de la même classe) sont accédés dans la même méthode, se demander si l’ensemble de ces accès doit être atomique. • si non, synchroniser séparément les accès des champs des divers objets public virement(double montant, Compte c) { synchronized (this) { this.solde -= montant; } synchronized(c) { c.solde += montant; } }

  26. Comment synchroniser (5) • si oui, ordonner les objets sur lesquels on synchronise : par exemple si on a public static int cpt = 0; public int monCpt = 0; public synchronized static incr() { cpt++; } public synchronized static decr() { cpt--; }

  27. Comment synchroniser (6) • si on veut que l’ensemble des deux incrémentations soit atomique, on fera : public synchronized void incrémenter() { synchronized (CompteTout.class) { incr(); monCpt++; } } • on a acquis d’abord le lock sur this puis sur CompteTout.class • une autre méthode (décrémenter, par exemple) devrait les acquérir dans le même ordre

  28. Comment synchroniser (7) • Le cas le plus difficile est quand on doit synchroniser deux objets de la même classe • Il faut alors ordonner ces objets • parfois un champs de l’objet le permet • sinon on utilisera System.identityHashCode()

  29. Comment synchroniser (8) • Si on doit locker les deux comptes lors d’un virement on pourra utiliser le numéro de compte: public virement(double montant, Compte c) { if (this.numCompte < c.numCompte) synchronized (this) { synchronized(c) { this.solde -= montant; c.solde += montant; } } else synchronized (c) { synchronized(this) { this.solde -= montant; c.solde += montant; } } }

  30. Attendre une ressource • La méthode wait() héritée d’Object • Doit être appelée sur un objet dont on a acquis le lock • Donc dans une méthode synchronized ou un bloc synchronized sur cet objet • le thread courant est placée sur une file d’attente liée à cet objet • il libère le lock sur l’objet durant le wait();

  31. Prévenir de la disponibilité d’une ressource • Les méthodes notify() et notifyAll() héritées d’Object • Doit être appelée sur un objet dont on a acquis le lock • Donc dans une méthode synchronized ou un bloc synchronized sur cet objet • notify() libère un thread en attente sur la file liée à l’objet • notifyAll() les libère tous mais un seul passera

  32. Bon usage de wait/notify • notify réveille un thread, mais pas nécessairement le bon • Il vaut mieux dans la plupart des cas utiliser notifyAll • Ne pas faire if (! condition) wait(); • si on est réveillé alors que condition n’est pas vraie on cesse d’attendre • Faire while (! condition) wait();

  33. Double wait() • Ne pas faire : wait(); wait(); • Car deux notify() pourraient se produire avant de sortir du premier wait(); le 2nd notify est alors perdu • Faire while (nombreDeNotify < 2) wait();

  34. Être sur de notifier • Pour être certain que le notify soit fait et qu'on libère bien ceux qui attendent même en cas de problème, toujours faire le notify dans un bloc finally try { … } finally { notifyAll(); // ou notify(); selon les cas }

  35. Double-checked Locking : NON • Comment synchroniser une lazy initialization • exemple (un singleton) : public static Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; }

  36. Double-checked Locking : NON • le double-checked locking ne fonctionne pas : public static Singleton getInstance() { if (instance == null) synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } return instance; } • un autre thread risque de ne pus trouver instance à null avant la fin de l’initialisation !

  37. Double-checked Locking : NON • Solution : synchroniser la méthode public static synchronized Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; }

  38. wait(), sleep() et synchronisation • On essaiera de libérer tous les moniteurs (et autres locks) avant de faire un wait() (sauf celui de l’objet sur lequel on wait();). Il y a un énorme risque de deadlock à ne pas le faire. • l’usage de variable locale est parfois utile, celles-ci ne devant pas être synchronisées • Dans le même ordre d’idée, on libérera tous les moniteurs, locks, … avant de faire un sleep();

  39. Timer et TimerTask (1) • Dans un contexte non graphique n’utilisez pas javax.swing.Timer !!! • Ici aussi, Timer est le contrôleur et TimerTask, la tâche class Tâche extends TimerTask { public void run() { System.out.println("action !"); } }

  40. Timer et TimerTask (2) • Timer timer = new Timer(); timer.schedule(new Tâche(), 5000); // la tâche s’exécutera dans 5 secondes timer.schedule(new Tâche(), 5000, 1000); // la tâche s’exécutera dans 5 secondes puis toutes les // secondes (en temps relatif, décalage possible) timer.scheduleAtFixedRate(new Tâche(), 5000, 1000); // la tâche s’exécutera dans 5 secondes puis toutes les // secondes (en temps absolu, non exécution possible) timer.cancel() // plus d’exécution après ça

  41. Concurrence en Java 5.0 • Le package java.util.concurrent fournit des classes utilitaires : • Semaphore • CyclicBarrier • CountDownLatch • Exchanger • diverses implémentation de l’interface BlockingQueue • l’interface Callable • FutureTask • Executor, ExecuterService et ThreadPoolExecutor

  42. BlockingQueue • boolean add(E e) -> IllegalStateException si plein • E remove() -> NoSuchElementException si vide • boolean offer(E e) -> false si plein • E poll() -> null si vide • void put(E e) -> attend si plein • E take() -> attend si vide

  43. BlockingQueue : producteur-consommateur (1) public class Producteur implements Runnable { private BlockingQueue<Integer> queue; private Random random = new Random(); public Producteur(BlockingQueue<Integer> queue) { this.queue = queue; } public void run() { while (true) { try { queue.put(random.nextInt(1000)); } catch (InterruptedException e) { } } } }

  44. BlockingQueue : producteur-consommateur (2) public class Consommateur implements Runnable { private BlockingQueue<Integer> queue; public Consommateur(BlockingQueue<Integer> queue) { this.queue = queue; } public void run() { while (true) { try { System.out.println(queue.take()); } catch (InterruptedException e) { } } } }

  45. BlockingQueue : producteur-consommateur (3) import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class Main { public static void main(String[] args) { BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(5); new Thread(new Producteur(queue)).start(); new Thread(new Consommateur(queue)).start(); } }

  46. Semaphore : théorie • Semaphore( int nombreDePermis); // 1 == binaire • Semaphore (int nombreDePermis, boolean FIFO); • void acquire(); // aussi avec un nbPermis • void acquireUninterruptibly(); // aussi avec un nbPermis • void release(); // aussi avec un nbPermis • boolean tryAcquire(); // aussi avec un nbPermis • boolean tryAcquire(long timeout, TimeUnit unités); // aussi avec un nbPermis (en 1er paramètre)

  47. Semaphore (1) public class Noeud implements Cloneable { private int valeur; private Noeud suivant; private Semaphore semaphore = new Semaphore(1); public Noeud(int valeur) { this(valeur, null); } public Noeud(int valeur, Noeud suivant) { this.valeur = valeur; this.suivant = suivant; } public Noeud getSuivant() { return suivant; }

  48. Semaphore (2) public void setSuivant(Noeud suivant) { this.suivant = suivant; } public int getValeur() { return valeur; } public void setValeur(int valeur) { this.valeur = valeur; } public Semaphore getSemaphore() { return semaphore; } }

  49. Semaphore (3) public class Liste { private Noeud tête; public boolean estVide() { return tête == null; } public String toString() { try { String rés = "[ "; for (Noeud n = tête; n != null; n = n.getSuivant()) { n.getSemaphore().acquire(); rés += n.getValeur() + " "; n.getSemaphore().release(); } rés += "]";return rés; } catch (InterruptedException e) { return null; } }

  50. Semaphore (4) public boolean ajouterNoeud(int valeur) { Noeud n = new Noeud(valeur); if (estVide()) { tête = n; return true; } try { Noeud prec = null; Noeud noeud = tête; for (; noeud != null; noeud = noeud.getSuivant()) { noeud.getSemaphore().acquire(); if (n.getValeur() == noeud.getValeur()) { if (prec != null) prec.getSemaphore().release(); noeud.getSemaphore().release(); return false; }

More Related