Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
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 • Quand la JVM donne la main à un thread, il load ses registres et stocke ceux du thread précédent. • Exemple : thread1 compte1.virer(100, compte2); thread2 compte2.virer(100, compte1); 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 Le problème (3) • L’exécution des 2 threads pourra donner : thread 1 R1 = compte1.solde; R1 -= 100; compte1.solde = R1; thread2 R1 = compte2.solde R1 -= 100; R1 = compte2.solde; R1 += 100; compte2.solde = R1 compte2.solde = R1; R1 = compte1.solde; R1 += 100; compte1.solde = R1 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. 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) 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 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 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 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 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; } } Arrêter un Thread (3) • En interrompant le Thread : public class MonThread extends Thread { public void run() { while (! isInterrupted()) { ….. } } } 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 Méthode synchronized • public synchronized int getCompteur() { return compteur; } public synchronized increment() { compteur++; } 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++; } } 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 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; } } 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 += ….. 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 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; } } 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; } } 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) 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 } 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 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; } } 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--; } 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 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() 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; }} } 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(); 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 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(); 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(); Ê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 } Double-checked Locking : NON • Comment synchroniser une lazy initialization • exemple (un singleton) : public static Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; } 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 ! Double-checked Locking : NON • Solution : synchroniser la méthode public static synchronized Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; } 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(); 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 !"); } } 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 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 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 BlockingQueue : producteurconsommateur (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) { } } } } BlockingQueue : producteurconsommateur (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) { } } } } BlockingQueue : producteurconsommateur (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(); } } 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) 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; } 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; } } 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; } } 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; } Semaphore (5) if (n.getValeur() < noeud.getValeur()) { if (prec == null) { n.setSuivant(noeud); tête = n; noeud.getSemaphore().release(); return true; } break; } if (prec != null) prec.getSemaphore().release(); prec = noeud; } n.setSuivant(noeud); prec.setSuivant(n); prec.getSemaphore().release(); if (noeud != null) noeud.getSemaphore().release(); return true; } catch (InterruptedException e) { return false;} } Semaphore (6) public boolean supprimerNoeud(int valeur) { if (estVide()) return false; try { Noeud prec = null; Noeud noeud = tête; for (; noeud != null; noeud = noeud.getSuivant()) { noeud.getSemaphore().acquire(); if (valeur == noeud.getValeur()) { if (prec == null) { tête = tête.getSuivant(); noeud.getSemaphore().release(); return true; } Semaphore (7) prec.setSuivant(noeud.getSuivant()); noeud.setSuivant(null); prec.getSemaphore().release(); noeud.getSemaphore().release(); return true; } // fin du if == if (prec != null) prec.getSemaphore().release(); prec = noeud; } prec.getSemaphore().release(); if (noeud != null) noeud.getSemaphore().release(); } catch (InterruptedException e) { } return false; } Semaphore (8) public class Main { public static void main(String[] args) { final Random random = new Random(); final Liste liste = new Liste(); new Thread() { public void run() { int cpt = 0; while (true) { liste.ajouterNoeud(random.nextInt(10)); System.out.println(++cpt + ") " + liste); } } }.start(); Semaphore (9) new Thread() { public void run() { int cpt = 0; while (true) { liste.supprimerNoeud(random.nextInt(10)); System.out.println(++cpt + ". " + liste); } } }.start(); } } CyclicBarrier : théorie • CyclicBarrier(int nbParties); • CyclicBarrier(int nbParties, Runnable action); • int await(); // à faire nbParties fois pour qu’action démarre (existe aussi avec un timeout) • reset(); // pour réutiliser la barrière CyclicBarrier (1) public class SommeLigne extends Thread { private int[] ligne; private int[] résultat; private int indice; private CyclicBarrier barrier; public SommeLigne(CyclicBarrier barrier, int[] ligne, int[] res, int idx) { this.barrier = barrier; this.ligne = ligne; this.résultat = res; this.indice = idx; } public void run() { for (int i = 0; i < ligne.length; i++) résultat[indice] += ligne[i]; try { barrier.await(); } catch (InterruptedException e) { } catch (BrokenBarrierException e) { } } } CyclicBarrier (2) public class Somme implements Runnable{ private int[] résultats; private int somme; public Somme(int[] résultats) { this.résultats = résultats; } public void run() { for (int i = 0; i < résultats.length; i++) somme += résultats[i]; System.out.println("La somme vaut : " + somme); } } CyclicBarrier (3) public class Main { public static void main(String[] args) { int [][] matrice = { { 1 }, { 2, 2 }, { 3, 3, 3 }, { 4, 4, 4, 4 }, { 5, 5, 5, 5, 5 } }; int nbLignes = matrice.length; int [] résultats = new int[nbLignes]; CyclicBarrier barrier = new CyclicBarrier(nbLignes, new Somme(résultats)); for (int i = 0; i < nbLignes; i++) new SommeLigne(barrier, matrice[i], résultats, i).start(); } CountDownLatch : théorie • CountDownLatch(int compteur); • void await(); // attends qu’on ait fait compteur countdown() (existe aussi avec un timeout) • void countdown(); // à faire compteur fois pour terminer CountDownLatch (1) public class SommeLigne extends Thread { private int[] ligne, résultat; private int indice; private CountDownLatch startLatch, stopLatch; public SommeLigne(CountDownLatch startLatch, CountDownLatch stopLatch, int[] ligne, int[] res, int idx) { this.startLatch = startLatch; this.stopLatch = stopLatch; this.ligne = ligne; this.résultat = res; this.indice = idx; } public void run() { try { startLatch.await(); } catch (InterruptedException e1) { } for (int i = 0; i < ligne.length; i++) résultat[indice] += ligne[i]; stopLatch.countDown(); } } CountDownLatch (2) public class Main { public static void main(String[] args) { int [][] matrice = { { 1 }, { 2, 2 }, { 3, 3, 3 }, { 4, 4, 4, 4 }, { 5, 5, 5, 5, 5 } }; int nbLignes = matrice.length; int [] résultats = new int[nbLignes]; CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch stopLatch = new CountDownLatch(nbLignes); for (int i = 0; i < nbLignes; i++) new SommeLigne(startLatch, stopLatch, matrice[i], résultats, i).start(); startLatch.countDown(); try { stopLatch.await(); } catch (InterruptedException e) { } new Thread(new Somme(résultats)).start(); } Callable <V> • • • • Semblable à Runnable ne définit pas de run() mais une méthode V call(); on l’utilisera avec une FutureTask Callable<Integer> public class SommeCall implements Callable<Integer> { private int[] résultats; private int somme; public SommeCall(int[] résultats) { this.résultats = résultats; } public Integer call() throws Exception { for (int i = 0; i < résultats.length; i++) somme += résultats[i]; return somme; } } FutureTask<V> : théorie • implémente Runnable et Future<V> • • • • FutureTask<V>(Callable<V> callable); FutureTask<V>(Runnable runn, V résultat); void run() appelle call() de callable ou run() de runn V get() attend la fin de run puis renvoie le résultat de call ou celui passé en paramètre • isDone(); • cancel(); • isCancelled(); FutureTask<Integer> (1) • utilisation avec une CyclicBarrier : FutureTask ft = new FutureTask<Integer>( new SommeCall(résultats)));; barrier = new CyclicBarrier(nbLignes, ft); for (int i = 0; i < nbLignes; i++) new SommeLigne(barrier, matrice[i], résultats, i).start(); try { System.out.println("la somme = " + ft.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } FutureTask<Integer> (2) • utilisation seule après l’emploi d’un CountDownLatch par exemple : FutureTask ft = new FutureTask<Integer>( new SommeCall(résultats)); ft.run(); // ou new Thread(ft).start(); try { System.out.println("la somme = " + ft.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } Exchanger<V> : théorie • Exchanger() • V exchange(V valeur) attend qu’un autre thread appelle exchange sur cet Exchanger et échange les valeur des deux threads existe aussi avec un timeout Exchanger<List<Integer>> • Un producteur remplit un buffer d’entier. • Quand il est plein, il échange ce buffer avec un consommateur. • Un consommateur vide un buffer plein • Quand il est vide il échange son buffer avec celui du producteur • Voir code sur l’extranet Executor et ExecutorService : théorie • Executor : Interface qui ne définit qu’une seule méthode – void execute(Runnable runnable); • ExecutorService : Interface, extends Executor – <T> List<Future<T>> invokeAll(Collection<Callable<T>> tasks); – <T> List<Future<T>> invokeAny(Collection<Callable<T>>tasks); – <T> Future<T> submit(Callable<T> task); – Future<?> submit(Runnable task); – <T> Future<T> submit(Runnable task, T résultat); – void shutdown(); Executor, ExecutorService : exemple class NetworkService { private final ServerSocket serverSocket; private final ExecutorService pool; public NetworkService(int port, int poolSize) throws IOException { serverSocket = new ServerSocket(port); pool = Executors.newFixedThreadPool(poolSize); } public void serve() { try { for ( ; ; ) pool.execute(new Handler(serverSocket.accept())); } catch (IOException ex) { pool.shutdown(); } } } java.util.concurrent.locks • définit les interfaces – Lock – Condition – ReadWriteLock • fournit les implémentations – ReentrantLock – ReentrantReadWriteLock Lock : théorie • • • • • • C'est une interface void lock(); // acquiert le lock void lockInterruptibly(); // de manière imterruptible Condition newCondition(); boolean tryLock(); // acquiert le lock s'il est libre boolean tryLock(long time, TimeUnit unit); // s'il devient libre dans le temps indiqué • void unlock(); // libère le lock • Elle est implémentée par la classe ReentrantLock • Exemple : la classe Buffer dans producerconsumer ReadWriteLock : théorie • Cette interface fournit 2 méthodes : – Lock readLock(); – Lock writeLock(); • Une classe qui l'implémente devra garder 2 locks – l'un partageable pour les opérations de lecture – l'autre exclusif pour les opérations d'écriture • Java fournit une implémentation : la classe ReentrantReadWriteLock. Celle-ci possède 2 classes emboîtées : – ReentrantReadWriteLock.ReadLock – ReentrantReadWriteLock.WriteLock ReentrantReadWriteLock : théorie (1) • on ne peut acquérir le readLock que si le writeLock est libre • on ne peut acquérir le writeLock que si le writeLock et le readLock sont libres • si on a le writeLock on peut acquérir le readLock • si on a le readLock, on ne peut pas acquérir le writeLock, on doit d'abord libérer le readLock • si des lecteurs ont le readLock et qu'un rédacteur demande le writeLock, plus aucun autre lecteur n'obtiendra le readLock ReentrantReadWriteLock : théorie (2) • ReentrantReadWriteLock() • ReentrantReadWriteLock(boolean fair) • Si le lock est construit avec fair == true, quand un writeLock est libéré, le writeLock sera accordé au rédacteur qui l'a demandé depuis le plus longtemps, à moins qu'il n'y ait des lecteurs qui attendent depuis encore plus longtemps auquel cas l'ensemble des lecteurs acquérra le readLock • Exemple : RWLockCoordinator de readerwriter Prévention des deadlocks • Un deadlock aura lieu uniquement si 4 conditions sont réalisées : – – – – Exclusion mutuelle Non préemption Hold and Wait Circularité • Pour prévenir un deadlock, on doit – soit empêcher la circularité, par exemple en ordonnant les ressources – soit empêcher le Hold and Wait, par exemple en acquérrant toutes les ressources simultanément ou en utilisant le Two Phase Locking • Exemples : le dîner des philosophes : voir philosophes