Introduction à Java - les « Threads » - Frédéric VOISIN – Nicole POLIAN FIIFO - « Remise à Niveau »
Les « Threads » Java (ou « processus légers ») Ils permettent de lancer plusieurs tâches en même temps au sein d’un même processus Ils permettent de refléter des découpages logiques en composants s’exécutant indépendamment ou en coopération Ils sont utilisés en interne par Java : le ramasse-miette est un thread de faible priorité, la méthode main(String[] args) s’exécute dans un thread … Les threads partagent la même zone mémoire, ce qui facilite la communication entre threads. Le temps de commutation entre threads est inférieur à celui des processus.
Exemple (presque) sans thread public class Train { int vitesse; String nom; public Train(int v, String n){ vitesse = v; nom = n;} public void run() { System.out.println(nom + " part"); try { Thread.currentThread().sleep(vitesse*500); } catch (InterruptedException e) {} System.out.println(nom + " roule"); Thread.currentThread().sleep(vitesse*50); System.out.println(nom + " s'arrete"); } La méthode main s’exécute dans un thread ! Le déroulement de sleep pourrait être abrégé par une « interruption », d’où le try/catch
Exemple sans thread (suite) // lancement de trois trains de vitesses différentes public class TestTrain { public static void main (String[] args) { Train micheline = new Train(50, "MICHELINE"); Train tgv = new Train(10, "TGV"); Train corail = new Train(20, "CORAIL"); micheline.run(); tgv.run (); corail.run(); System.out.println("fin du main"); } MICHELINE part MICHELINE roule MICHELINE s'arrete TGV part TGV roule TGV s'arrete CORAIL part CORAIL roule CORAIL s'arrete fin du main
Exemple avec thread public class Test { public static void main(String[] args) { Train tgv = new Train(10, "TGV"); Train corail = new Train(20,"CORAIL"); Train micheline = new train (50, "MICHELINE"); micheline.start(); tgv.start(); corail.start(); System.out.println("Threads actifs : " + Thread.currentThread().activeCount()); try { corail.join(); // attendre la fin de corail ! } catch (InterruptedException ex) { } System.out.println("fin du main"); }
Exemple avec thread (suite) class Train extends Thread { int vitesse; public Train(int v, String n) { super(n); // permet de « nommer » le thread. // getName() renvoie le nom ! vitesse = v; } public void run() { System.out.println(getName() + " part"); try { sleep(vitesse*500); } catch(InterruptedException e) { } System.out.println(getName() + " roule"); try { sleep(vitesse*50); System.out.println(getName() + " s'arrete"); Exemple d’exécution Threads actifs : 4 MICHELINE part TGV part CORAIL part TGV roule TGV s'arrete CORAIL roule CORAIL s'arrete Threads actifs : 2 fin du main MICHELINE roule MICHELINE s'arrete
Le principe des Threads La classe Train dérive de la classe Thread. La méthode start démarre le thread, ce qui entraîne l’appel de la méthode run. Le thread termine quand il atteint la fin de la méthode run. La méthode join permet de se synchroniser avec la fin d’un thread. Une autre synchronisation serait nécessaire si le thread produisait des résultats à récupérer (à voir après) L’héritage multiple n’existant pas en Java, il existe un deuxième mécanisme pour utiliser les threads : l’utilisation de l’interface Runnable, qui impose l’existence d’une méthode run. On passe alors une instance d’une classe qui implémente Runnable comme premier argument au constructeur de Thread.
Utilisation de l’interface Runnable public class Test { public static void main(String[] args) { Thread tgv = new Thread(new Train(10, "TGV")); Thread corail = new Thread(new Train(20, "CORAIL")); Thread micheline = new Thread(new Train(50, "MICHELINE")); micheline.start(); tgv.start(); corail.start(); System.out.println("fin du main") ; } // le paramètre du constructeur de Thread doit implémenter l’interface Runnable !
Utilisation de l’interface Runnable (suite) public class Train implements Runnable { int v; String nom; public Train(int i, String n) { v = i; nom = n;} public void run() { System.out.println(nom + " part"); try { Thread.currentThread().sleep(v*500); } catch(InterruptedException e) {} System.out.println(nom + " roule"); Thread.currentThread().sleep(v*50); System.out.println(nom + " s'arrete"); } fin du main MICHELINE part TGV part CORAIL part TGV roule TGV s'arrete CORAIL roule CORAIL s'arrete MICHELINE roule MICHELINE s'arrete
Les états d’un Thread new ( ) créé start ( ) wait ( ) ou sleep ( ) actif En attente mort new ( ) terminaison start ( ) wait ( ) ou sleep ( ) notify ( ) ou interruption
Les principales méthodes liées aux threads statiques : currentThread() activeCount() // threads démarrés et pas encore morts enumerate() // permet de les copier dans un tableau // Thread[] th=new Thread[nb];nb=Thread.enumerate(th); yield() // rend la main volontairement pour un autre thread sleep() // ne relâche pas les moniteurs (cf. infra) non statiques isAlive() join() setDaemon() // à faire avant start() ! isDaemon() dans Object // pour les moniteurs (synchronisation de threads) wait // en attente de notification notify // notification « Deprecated » (induisent de forts risques de deadlock, cf. infra) stop, suspend, resume
Exemple de Thread public class Heure1 extends Frame { // utilisation de AWT !! Label l1, l2, h1, h2; Button arret1; Timer1 t1, t2; Heure1() { setLayout(new FlowLayout()); l1 = new Label (" "); l2 = new Label (" "); l1.setBackground(Color.white); l2.setBackground(Color.white); l1.setAlignment(Label.CENTER); l2.setAlignment(Label.CENTER); Font f = new Font("TimesRoman", Font.PLAIN, 18); setFont(f); arret1 = new Button ("STOP"); h1 = new Label("MONTREAL"); h2 = new Label("PARIS"); ActionListener larret1 = new ActionListener() { public void actionPerformed(ActionEvent e){ System.exit(1);} }; // classe interne, définie au vol ! … // ActionListener est une interface associée aux événements click sur les boutons
Exemple de Thread (suite) arret1.addActionListener(larret1); // associe action/bouton add(arret1); // ajout des composants add(l1); add(h1); // à l’instance de Frame add(l2); add(h2); t1 = new Timer1(l1, 1); t2 = new Timer1(l2, 2); t1.start(); t2.start(); } // fin de Heure1() public static void main(String[] args) { Heure1 t = new Heure1(); t.setBounds(20, 20, 800, 60); t.setVisible(true); }
Exemple de Thread (suite) public class Timer1 extends Thread { Label l; int type; Date maDate; SimpleDateFormat mF = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss"); public Timer1(Label l, int t) { this.l = l; type = t; } public void run() { while (true) { try { sleep (2000); } catch(InterruptedException e) {} maDate = new Date(); if (type == 1) { mF.setTimeZone(TimeZone.getTimeZone("America/Montreal")); L.setText(mF.format(maDate)); } else { mF.setTimeZone(TimeZone.getTimeZone("Europe/Paris")); }
Les démons (« daemon ») Il existe deux sortes de threads deux méthodes les threads ordinaires (utilisateurs) les threads démons (notamment créés par la JVM; exemple : le ramasse-miettes) deux méthodes setDaemon(boolean b) // à exécuter avant start boolean isDaemon() Un « processus » Java s’arrête lorsqu’il ne reste plus que des threads démons. Ceux-ci correspondent à des threads « de service » qui ne contribuent pas directement à la logique applicative.
L’ordonnancement (scheduling) Question : quel thread a la main ? celui qui a la plus grande priorité parmi les threads actifs ! pour changer la priorité : getPriority() setPriority() Thread.MIN_PRIORITY Thread.MAX_PRIORITY Thread.NORM_PRIORITY En cas d’égalité, certaines implémentation laissent la main au thread en cours (pas d’équité entre les threads potentiellement activables). Dans ce cas, il faut utiliser la méthode yield() ou bien sleep(0) pour relâcher la main.
Arrêter un Thread } // accès sans précaution à arrete ;-( public class TestArret { public static void main(String[] args) { tt = new Train(); tt.start(); try { Thread.currentThread().sleep(2000); tt.stoppe(); tt.join(); } catch (InterruptedException ex) {} System.out.println("fin du main"); } class Train extends Thread { private boolean arrete = false; public void run() { while (! arrete) { System.out.println("le train roule"); try { sleep(500);} catch(InterruptedException e) {} System.out.println("le train s'arrete"); public void stoppe() { arrete = true ; } } // accès sans précaution à arrete ;-( le train roule le train s'arrete fin du main
Problèmes de synchronisation Les threads partagent les mêmes données et agissent sur les mêmes objets problèmes d’accès incohérents aux données On peut protéger une suite d’instruction (ou une fonction) avec synchronized Pour une fonction : à tout instant il s’exécute au plus une fonction synchronized sur un objet donné… mais les fonctions non synchronized n’ont aucune obligation synchronized int fonct() { … } Par « verrouillage » d’un objet (arbitraire) pour une suite d’instructions arbitraire : synchronized (obj) { // au plus une séquence d’instructions ou méthode // synchronized à la fois sur obj. obj.action(); … Reprend les principes des moniteurs de Hoare/Brinch Hansen
Exemple de synchronisation class Truc { int val = 0; void incr(int tempo) { int i = val; try { Thread.currentThread().sleep(tempo); } catch (InterruptedException e) {} val = i+1; // la valeur de val il y a un certain temps ! try { Thread.currentThread().sleep(tempo) ; } catch (InterruptedException ex) { } ; } public String toString() { return "valeur " + val;}} class Collision { static truc cpt = new truc(); public static void main(String[] arg) { Compteur t1 = new Compteur(cpt,1); Compteur t2 = new Compteur(cpt,2); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException ex) {}; System.out.println("fin du main : " + cpt); }}
Exemple de synchronisation (suite) class Compteur extends Thread { int num; truc cpt; Random rd; public Compteur (truc c, int nm) { num = nm; cpt = c; rd = new Random(nm); } public void run() { System.out.println("debut run " + num); for(int i = 0; i<100; i++) { // synchronized(cpt) { cpt.incr(Math.abs(rd.nextInt())); System.out.println ("fin run " + num); sans synchronized(cpt) debut run 1 debut run 2 fin run 1 fin run 2 fin du main : valeur 143 avec synchronized(cpt) fin du main : valeur 200
Les appels wait et notify wait bloque l’exécution du thread jusqu’à réception d’une « notification » envoyée par un autre thread. Le verrou sur l’objet est levé (à la différence de sleep !) notify envoie la notification précédente : « réveil » du (ou plutôt d’un) thread bloqué. notifyAll() réveille tous les threads bloqués sur cet objet. Attention: les threads débloqués doivent encore « acquérir » l’objet sur lequel ils se synchronisent. Ils sont en concurrence avec tous les autres threads similaires (pas de privilège d’accès). Souvent on doit englober le wait dans une boucle d’accès en attendant qu’une certaine condition soit effectivement réalisée. Attention aux deadlocks, si on acquiert les objets dans le désordre ou si on oublie d’en libérer l’accès !
wait et notify (suite) synchronized ( obj ) { obj.wait ( ) ; } obj.notify ( ) ; T1 . . T2 . . . . . réveil L’attente se fait « sur » un objet. Le notify est envoyé à ce même objet. L’objet doit « appartenir » au thread qui exécute wait/notify, cela implique l’utilisation obligatoire de synchronized. Gérer le verrou sur un objet nécessite d’acquérir cet objet !
Un Exemple … public class Heure2 extends Frame { Label l1, l2; Button h1, h2, arret1; Timer2 t1, t2; Heure2() { setLayout(new FlowLayout()); l1 = new Label(" "); l2 = new Label(" "); l1.setBackground(Color.white); l2.setBackground(Color.white); l1.setAlignment(Label.CENTER); l2.setAlignment(Label.CENTER); Font f = new Font("TimesRoman", Font.PLAIN, 18); setFont(f); arret1 = new Button("Stop Montreal"); h1 = new Button("MONTREAL"); h2 = new Button("PARIS"); ActionListener larret1 = new ActionListener() { public void actionPerformed(ActionEvent e) { t1.stoppe(); // arret definitif de l'horloge locale remove(l1); remove(h1); remove(arret); }}; …
Un Exemple (suite) ActionListener lh2 = new ActionListener() { public void actionPerformed(ActionEvent e) { if (! t2.isAlive()) { t2.start(); } }} ; ActionListener lh1 = new ActionListener() { if (! t1.isAlive()) { t1.start(); } else { t1.bascule(); } }}; arret1.addActionListener(larret1); h1.addActionListener(lh1; h2.addActionListener(lh2); add(arret1); add(l1); add(h1); add(l2); add(h2); t1 = new Timer2(l1, 1); t2 = new Timer2(lab2, 2); } public static void main(String[] arg) { Heure2 t = new Heure2(); t.setBounds(20, 20, 800, 60); t.setVisible(true);
Un Exemple (suite) class Timer2 extends Thread { Label L; int type; boolean marche, actif; public Timer2(Label l, int t) { L = l; type = t; marche = true; actif = true; } public void stoppe() { marche = false; } public void bascule() { if (actif) { actif = false ; } else { synchronized(L) { L.notify(); } } }
Un Exemple (suite) public void run() { Date maDate; SimpleDateFormat mF=new SimpleDateFormat("dd-MMM-yyyyHH:mm:ss"); while (marche) { try { sleep (2000); } catch (InterruptedException ex) {} if (! actif) { try { synchronized(L) { L.wait(); } } catch (InterruptedException ex) {} actif = true ; } maDate = new Date(); if (type == 1){ mF.setTimeZone(TimeZone.getTimeZone("America/Montreal")); L.setText(monFormat.format(maDate)); } else { mF.setTimeZone(TimeZone.getTimeZone("Europe/Paris")); L.setText(mF.format(maDate)); } }}}
Un Exemple (affichage)