Vous connaissez probablement déjà le multitâche : la possibilité d'avoir plusieurs programmes travaillant en même temps. Par exemple, il est possible avec un système multitâche d'imprimer un document en même temps que vous en modifiez un autre ou que vous téléchargez vos e-mails. Aujourd'hui, vous disposez probablement d'un ordinateur avec plusieurs coeurs dans le même processeur, mais le nombre d'actions s'exécutant simultanément peut être beaucoup plus conséquent et ne se limite pas au nombre de coeurs effectifs.
Le système d'exploitation affecte une tranche de temps à chaque action, ce qui donne l'illusion d'une activité parallèle. Cette répartition des ressources est possible parce que la plupart des programmes ne se servent pas de l'intégralité du temps machine. Par exemple, lorsqu'un utilisateur saisit des données rapidement, il ne prend qu'un vingtième de seconde par caractère.
Les programmes utilisant plusieurs threads développent l'idée du multitâche en l'implémentant à un niveau plus bas : des programmes individuels peuvent effectuer plusieurs tâches en même temps. Chaque tâche est traditionnellement appelée un thread. Les programmes qui peuvent exécuter plusieurs threads en même temps sont appelés des programmes à multithreads.
Chaque thread peut être considéré comme possédant un contexte indépendant (un contexte est composé d'un processeur, d'un ensemble de registres, de mémoire et d'un code particulier).
Quelle est donc la différence entre plusieurs processus
et plusieurs threads ? La différence
essentielle est qu'un processus possède une copie unique de ses propres
variables, alors que les threads partagent les mêmes données. Cela
peut paraître risqué, et cela l'est parfois. Mais il est bien plus
rapide de créer et de détruire des threads individuels que de
créer un nouveau processus. C'est pourquoi tous les systèmes d'exploitation
modernes supportent les multithreads. De plus, la communication entre les processus
est bien plus lente et plus restrictive qu'entre des threads.
Les multithreads sont très utiles dans la pratique. Par exemple, un navigateur doit pouvoir gérer plusieurs hôtes, ouvrir une fenêtre de courrier électronique, afficher une nouvelle page et télécharger des données en même temps. Le langage de programmation java lui-même se sert d'un thread pour gérer le ramasse-miettes en tâche de fond, ce qui vous épargne de gérer la mémoire vous-même ! Les programmes ayant une interface utilisateur graphique possèdent un thread séparé pour récupérer les événements de l'interface utilisateur.
Commençons par étudier un programme qui ne se sert pas de plusieurs threads et qui, par conséquent, empêche l'utilisateur d'effectuer plusieurs tâches avec ce programme. Ce programme fait rebondir une balle en la déplaçant en permanence. Si la balle arrive contre le mur, il la fait rebondir.
package threads; import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.util.ArrayList; import javax.swing.*; public class SimpleBalle extends JFrame { private JButton démarrer = new JButton( ); private JButton fermer = new JButton( ); private JPanel boutons = new JPanel(); private Panneau panneau = new Panneau(); public SimpleBalle() { super( ); panneau.setBackground(Color.ORANGE); add(panneau); add(boutons, BorderLayout.SOUTH); boutons.add(démarrer); boutons.add(fermer); démarrer.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ajoutBalle(); } }); fermer.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.exit(0); } }); setSize(500, 400); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } private void ajoutBalle() { try { Balle balle = new Balle(); panneau.ajout(balle); for (int i=0; i<1000; i++) { balle.déplace(panneau.getBounds()); panneau.paintComponent(panneau.getGraphics()); Thread.sleep(5); } } catch (InterruptedException ex) { } } private class Panneau extends JPanel { private ArrayList<Balle> balles = new ArrayList<Balle>(); public void ajout(Balle balle) { balles.add(balle); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D surface = (Graphics2D) g; for (Balle balle : balles) surface.fill(balle.getForme()); } } private class Balle { private double x, y, dx=5, dy=5; public void déplace(Rectangle2D zone) { x+=dx; y+=dy; if (x < zone.getMinX()) { x = zone.getMinX(); dx = -dx; } if (x+15 >= zone.getMaxX()) { x = zone.getMaxX() - 15; dx = -dx; } if (y < zone.getMinY()) { y = zone.getMinY(); dy = -dy; } if (y+15 >= zone.getMaxY()) { y = zone.getMaxY() - 15; dy = -dy; } } public Ellipse2D getForme() { return new Ellipse2D.Double(x, y, 15, 15); } } public static void main(String[] args) { new SimpleBalle(); } }
Dès que vous cliquez sur le bouton "Démarrer", le programme lance la balle à partir du coin supérieur gauche de l'écran et celle-ci commence à rebondir. Le gestionnaire du bouton "Démarrer" appelle la méthode ajoutBalle(). Cette méthode contient une boucle progressant sur 1000 déplacements. Chaque appel à la méthode déplace() déplace légèrement la balle, ajuste la direction si elle rebondit sur un mur, puis redessine l'écran.
La méthode statique sleep() de la classe Thread effectue une pause du nombre de millisecondes donné.
§
L'appel à Thread.sleep() ne crée pas un nouveau thread, sleep() est une méthode statique de la classe Thread qui met le thread courant en sommeil (5 ms). Bien sûr, vous avez la possiblité de changer la durée du sommeil pour obtenir l'apparence d'un déplacement plus ou moins rapide. La méthode sleep() peut déclencher une exception InterruptedException, il faut donc utiliser un bloc try-catch si nous désirons la capturer.
Nous reviendrons sur cette exception et sur son gestionnaire un peu plus tard. Pour l'instant, nous terminons simplement le rebond si l'exception survient.
§
Si vous étudiez attentivement le code, vous remarquez l'appel panneau.paintComponent(panneau.getGraphics()); dans la méthode ajoutBalle() de la classe SimpleThread. Cela est assez étrange, vous devriez normalement appeler la méthode repaint() et laisser le soin à AWT d'obtenir le contexte graphique et de redessiner l'écran. Toutefois, si vous essayez de réaliser cet appel dans ce programme, vous découvrirez que l'écran n'est jamais redessiné puisque la méthode ajoutBalle() a totalement pris le pas sur le traitement.
Remarquez également que le composant de balle étend JPanel, ce qui simplifie l'effacement de l'arrière-plan. Dans le programme suivant, nous utiliserons un autre thread pour calculer la position de la balle et nous emploierons à nouveau la méthode repaint() de JComponent.
A l'évidence, ce n'est pas une situation acceptable et nous ne souhaitons pas que les programmes utilisés se comportent de cette manière lorsqu'ils doivent effectuer des tâches plutôt longues. Après tout, lorsque vous lisez des données en passant par une connexion réseau, il est trop courant d'être bloqué par une tâche gourmande en temps machine, que vous souhaiteriez vraiment interrompre.
Par exemple, supposons que vous chargiez une grande image et que vous décidiez, après avoir vue une partie, que vous ne voulez pas voir le reste. Il peut alors être très intéressant de pouvoir cliquer sur un bouton "Arrêter" ou "Précédent" pour interrompre le chargement. Dans le prochain chapitre, nous montrerons comment laisser cette possiblité à l'utilisateur en exécutant les parties critiques d'un programme dans un thread séparé.
Nous allons rendre notre programme de balles plus attentif à l'utilisateur en exécutant le code qui déplace la balle dans un thread séparé. Vous pourrez en fait lancer plusieurs balles. Chacune est déplacée dans son propre thread. En outre, le thread de distribution d'événements AWT continue à fonctionner en parallèle, il s'occupe des événements de l'interface utilisateur.
Chaque thread ayant une occasion de s'exécuter, le thread de répartition d'événements peut savoir si l'utilisateur clique sur le bouton "Fermer" alors que les balles rebondissent. Il peut alors traiter l'action correspondante.
Comme
la plupart des ordinateurs ne possèdent pas plusieurs processeurs, la
machine virtuelle java se sert d'un mécanisme dans lequel chaque thread a une chance d'être exécuté pendant un bref moment, avant
qu'un autre thread soit activé. La machine
virtuelle se fonde en général sur le système d'exploitation
hôte pour la gestion des threads, et à
priorité égale, les threads se partagent à part égale le temps d'exécution :
Nous utilisons l'exemple de la balle rebondissante pour bien demontrer visuellement le besoin de simultanéité. En règle général, inquiétez-vous des calculs long. Votre calcul fera sûrement partie d'un cadre plus général, par exemple l'interface graphique ou le Web. Dès que le cadre appelle l'une de vos méthodes, on s'attend généralement à un retour rapide. Pour toutes les tâches plutôt longues, utilisez systématiquement un thread séparé.
Dans notre prochain programme, nous nous servirons de deux threads. Le premier
s'occupe de la balle, et le second (le thread principal)
s'occupe de l'interface utilisateur. Comme chaque thread a une chance d'être
exécuté, le thread principal en a une de voir que vous cliquez
sur le bouton "Fermer", même si
la balle continue de rebondir. Il peut alors traiter l'action de fermeture.
Voici une procédure très simple pour exécuter du code dans un thread séparé :
class MaClasse implements Runnable { public void run() { // code de la tâche à réaliser } }
Runnable interface = new MaClasse();
Thread tâche = new Thread(interface);
tâche.start();
Pour exécuter notre programme dans un thread séparé, nous devons simplement implémenter une classe BalleSéparée et placer le code de l'animation dans la méthode run(), comme dans le code suivant :
private class BalleSéparée implements Runnable { private Balle balle; public BalleSéparée(Balle balle) { this.balle = balle; } public void run() { try { for (int i=0; i<1000; i++) { balle.déplace(panneau.getBounds()); panneau.repaint(); Thread.sleep(5); } } catch (InterruptedException ex) { } } }
Une fois de plus, nous devons intercepter une exception appelée InterruptedException que la méthode sleep() menace de déclencher. Nous découvrirons cette exception dans le prochain chapitre. Typiquement, l'interruption sert à demander la fin d'un thread. De même, notre méthode run() se termine lorsqu'une InterruptedException se présente.
private void ajoutBalle() { Balle balle = new Balle(); panneau.ajout(balle); new Thread(new BalleSéparée(balle)).start(); }
C'est tout ce qu'il faut savoir ! vous savez maintenant lancer des tâches en parallèles. Le reste de cette étude vous montrera comment contrôler l'interaction entre les threads.
package threads; import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.util.ArrayList; import javax.swing.*; public class PlusieursBalles extends JFrame { private JButton démarrer = new JButton( ); private JButton fermer = new JButton( ); private JPanel boutons = new JPanel(); private Panneau panneau = new Panneau(); public PlusieursBalles() { super( ); panneau.setBackground(Color.ORANGE); add(panneau); add(boutons, BorderLayout.SOUTH); boutons.add(démarrer); boutons.add(fermer); démarrer.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ajoutBalle(); } }); fermer.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.exit(0); } }); setSize(500, 400); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } private void ajoutBalle() { Balle balle = new Balle(); panneau.ajout(balle); new Thread(new BalleSéparée(balle)).start(); } private class BalleSéparée implements Runnable { private Balle balle; public BalleSéparée(Balle balle) { this.balle = balle; } public void run() { try { for (int i=0; i<1000; i++) { balle.déplace(panneau.getBounds()); panneau.repaint(); Thread.sleep(5); } } catch (InterruptedException ex) { } } } private class Panneau extends JPanel { private ArrayList<Balle> balles = new ArrayList<Balle>(); public void ajout(Balle balle) { balles.add(balle); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D surface = (Graphics2D) g; for (Balle balle : balles) surface.fill(balle.getForme()); } } private class Balle { private double x, y, dx=5, dy=5; public void déplace(Rectangle2D zone) { x+=dx; y+=dy; if (x < zone.getMinX()) { x = zone.getMinX(); dx = -dx; } if (x+15 >= zone.getMaxX()) { x = zone.getMaxX() - 15; dx = -dx; } if (y < zone.getMinY()) { y = zone.getMinY(); dy = -dy; } if (y+15 >= zone.getMaxY()) { y = zone.getMaxY() - 15; dy = -dy; } } public Ellipse2D getForme() { return new Ellipse2D.Double(x, y, 15, 15); } } public static void main(String[] args) { new SimpleBalle(); } }
class MaClasse extends Thread { public void run() { // code de la tâche à réaliser } }
Vous construisez ensuite un objet de la sous-classe et appelez sa méthode start(). Toutefois, cette approche n'est plus conseillée. Il vaut mieux découpler la tâche qui doit être exécutée en parallèle de son mécanisme d'exécution. Si vous disposez de plusieurs tâches, créer un thread séparé pour chacune serait trop onéreux. Vous pouvez plutôt utiliser un pool de threads (voir le chapitre Executors, plus loin dans cette étude).
ATTENTION : N'appelez pas la méthode run() de la classe Thread ou de l'objet Runnable car cela exécute directement la tâche sur le même thread. Aucun nouveau thread n'est démarré. Appelez plutôt la méthode start(). Elle créera un nouveau thread qui exécutera la méthode run() quand cela lui sera permis.
Un thread s'arrête naturellement lorsque sa méthode run() se termine, en exécutant une instruction return après la dernière instruction du corps de la méthode ou si une exception survient qui ne soit pas interceptée dans la méthode.
Dans la dernière version de Java, il existait également une méthode stop() qu'un autre thread aurait pu appeler pour terminer un thread. Mais cette méthode est aujourd'hui obsolète (voir plus loin dans ce chapitre).
Lorsque la méthode interrupt() est appelée sur un thread, l'indication du thread est définie sur interrompu (interrupted). Il s'agit d'un indicateur booléen présent dans chaque thread. Chaque thread doit, à l'occasion, vérifier s'il a été interrompu.
Pour savoir si l'indication a été définie sur interrompu, appelez la méthode currentThread() de la classe Thread pour obtenir le thread actuel, puis appelez la méthode isInterrupted() :
while (!Thread.currentThread().isInterrupted() && encore du tavail) { // faire le travail }
Attention, un thread ne fonctionne pas en permanence. Il doit se rendormir ou attendre régulièrement, pour donner aux autres threads une chance d'être exécutés. Du coup, lorsqu'un thread dort, il ne peut pas déterminer s'il est interrompu. Lorsque la méthode interrupt() est appelée sur un thread actuellement bloqué, l'appel bloquant (comme sleep() ou wait()) est terminé par une InterruptedException.
Il n'existe aucune spécification demandant qu'un thread doit être terminé. L'interruption d'un thread se contente d'attirer son attention. Le thread interrompu peut choisir comment réagir devant l'interruption. Certains threads sont tellement importants qu'ils peuvent tout simplement ignorer leur interruption en continuant leur travail. Mais, plus couramment, un thread essaiera d'interpréter l'interruption comme une demande d'arrêt.
La méthode run() de ce type de thread a la forme suivante :public void run() { try { ... while (!Thread.currentThread().isInterrupted() && encore du tavail) { // faire le travail } } catch(InterruptedException ex) { // le thread a été interrompu pendant sleep() ou wait() } finally { // nettoyage, si nécessaire } // sort de la méthode run() et met fin au thread }
La vérification isInterrupted() n'est ni nécessaire ni utile si vous appelez la méthode sleep() (ou une autre méthode pouvant être interrompue) après chaque itération. Si vous appelez la méthode sleep() lorsque l'indication est définie sur interrompu, elle ne se met pas en veille. Elle efface le status (!) et lance une exception InterruptedException.
Ainsi, si votre boucle appelle sleep(), ne vous inquiétez pas de vérifier l'interruption et interceptez simplement l'exception, comme ceci :
public void run() { try { ... while (encore du tavail) { // faire le travail Thread.sleep(délai); } } catch(InterruptedException ex) { // le thread a été interrompu pendant le sommeil } finally { // nettoyage, si nécessaire } // sort de la méthode run() et met fin au thread }
Curieusement, il existe deux méthodes très similaires, interrupted() et isInterrupted(). La méthode interrupted() est une méthode statique qui vérifie si le thread actuel a été interrompu. De plus, un appel à la méthode interrupted() réinitialise l'indication "interrompu" d'un thread. D'un autre côté, la méthode isInterrupted() est une méthode d'instance que vous pouvez utiliser pour vérifier que n'importe quel thread a été interrompu. L'indication "interrompu" de son attribut n'est pas modifié.
ATTENTION : Pour l'instant, notre clause catch est vierge. Ne le laissez pas comme cela ! Si vous ne trouvez rien à faire dans la clause catch, il reste un choix résonnable qui consiste à prévenir que le thread courant vient de s'arrêter inopinément au moyen de la commande Thread.currentThread().interrupt().
Nous allons mettre en oeuvre toute cette technique d'interruption à l'aide du programme précédant. Toutefois, il n'existera qu'une seule balle dans l'application qui rebondit indéfiniment sauf avis contraire.
Comme nouveauté, nous avons également un bouton "Arrêter" qui permet d'interrompre le thread séparé (représenté par le panneau) qui va finir par ne plus exister (le panneau lui-même est toujours présent). Après avoir interrompu le thread, un nouveau thread est créé afin de remplacer l'ancien. Lorsque vous cliquez sur le bouton "Démarrer", si le thread n'est pas encore en mode d'exécution, il va être lancé au moyen de la méthode start().
package threads; import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.util.ArrayList; import javax.swing.*; public class SimpleBalle extends JFrame { private JButton démarrer = new JButton( ); private JButton arrêter = new JButton( ); private JPanel boutons = new JPanel(); private Panneau panneau = new Panneau(); private Thread tâcheSéparée = new Thread(panneau); public SimpleBalle() { super( ); panneau.setBackground(Color.ORANGE); add(panneau); add(boutons, BorderLayout.SOUTH); boutons.add(démarrer); boutons.add(arrêter); démarrer.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (!tâcheSéparée.isAlive()) tâcheSéparée.start(); } }); arrêter.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { tâcheSéparée.interrupt(); tâcheSéparée = new Thread(panneau); } }); setSize(500, 400); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } private class Panneau extends JPanel implements Runnable { private Balle balle = new Balle(); @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D surface = (Graphics2D) g; surface.fill(balle.getForme()); } public void run() { try { while (!Thread.currentThread().interrupted()) { balle.déplace(getBounds()); repaint(); Thread.sleep(4); } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } private class Balle { private double x, y, dx=5, dy=5; public void déplace(Rectangle2D zone) { x+=dx; y+=dy; if (x < zone.getMinX()) { x = zone.getMinX(); dx = -dx; } if (x+15 >= zone.getMaxX()) { x = zone.getMaxX() - 15; dx = -dx; } if (y < zone.getMinY()) { y = zone.getMinY(); dy = -dy; } if (y+15 >= zone.getMaxY()) { y = zone.getMaxY() - 15; dy = -dy; } } public Ellipse2D getForme() { return new Ellipse2D.Double(x, y, 15, 15); } } public static void main(String[] args) { new SimpleBalle(); } }
Les threads peuvent se trouver dans l'un des six états qui suivent.
Pour connaître l'état actuel, appelez simplement la méthode getState().
§
Lorsqu'un thread s'exécute, il ne poursuit pas obligatoirement l'opération dans le temps. En réalité, cela peut se révéler souhaitable si des threads sont mis en pause de temps à autre afin que d'autres threads aient une occasion de s'exécuter.
Ce mécanisme dépend directement du système d'exploitation. La plupart des systèmes accordent à chaque thread une tranche de temps pour effectuer sa tâche. Lorsque cette tranche de temps est écoulée, le système d'exploitation passe a un autre thread.
Lorsque vous sélectionnez le thread suivant, le système d'exploitation prend en compte les priorités du thread. Voir plus loin pour en savoir plus sur les priorités.
Sur une machine multiprocesseur, chaque processeur peut exécuter un thread et plusieurs méthodes threads peuvent s'exécuter réellement en parallèle. Bien entendu, s'il y a plus de threads que de processeurs, le gestionnaire est toujours responsable du découpage en tranches.
N'oubliez jamais qu'un
thread exécutable peut être ou non en cours d'exécution.
§
les verrous java.util.concurrent et les verrous d'objets sont abordés plus loin dans ce chapitre.
§
Lorsqu'un thread est bloqué ou en attente (ou, bien sûr, lorsqu'il se termine), un autre thread est prévu pour être exécuté. Lorsqu'un thread est réactivé (par exemple, parce que sa temporisation a expiré ou qu'il a réussi à acquérir un verrou), le gestionnaire regarde s'il possède une priorité supérieure à celle du thread actuellement en cours d'exécution. Dans ce cas, il interrompt le thread actuel et sélectionne un nouveau thread.
En particulier, il est possible de tuier un thread en invoquant la méthode stop(). Cette méthode déclenche une erreur ThreadDeath qui tue le thread. Cependant, la méthode stop() n'est plus utilisée et il faut éviter de l'appeler dans votre code.
Pour savoir si un thread est couramment actif, c'est à dire qu'il est soit exécutable , soit bloqué, utilisez la méthode isAlive(). Cette méthode renvoie true si le thread est exécutable ou bloqué, et false si le thread est nouveau et pas encore exécutable, ou si le thread est mort.
Dans les sections suivantes, nous verrons les diverses propriétés des threads :
Dans le langage de programmation Java, chaque thread possède une priorité. Par défaut, un thread hérite de la priorité du thread qui l'a construit. Vous pouvez augmenter ou baisser la priorité de n'importe quel thread avec la méthode setPriority(). La priorité de n'importe quel thread peut être choisi entre :
Lorsque que le gestionnaire de thread peut choisir un nouveau thread, il choisit généralement le thread de la plus haute priorité. Toutefois, les priorités de threads sont fortement dépendantes du systtème. Lorsque la machine virtuelle compte sur l'implémentation des threads de la plate-forme hôte, les priorités de threads Java sont mis en correspondance avec les niveaux de priorité de la plate-forme hôte, qui peut en avoir plus ou moins.
A titre d'exemple, Windows affiche sept niveaux de priorité. Certaines propritétés Java concorderont avec le même niveau du système d'exploitation. Dans la machine virtuelle de Sun pour Linux, les priorités de threads sont totalement ignorées : tous les threads ont systématiquement la même priorité.
Les programmeurs débutants font parfois trop appel aux priorités de thread. Il existe peu de raisons de tirer un trait sur les priorités, mais ne structurez jamais vos programmes en fonction du niveau de priorité.
ATTENTION : Si vous souhaitez tout de même utiliser les priorités, soyez averti d'une erreur commune aux débutants. Si vous diposez de plusieurs threads à haute priorité qui ne reviennent pas inactifs, ceus de moindre priorité risquent de ne jamais s'exécuter. Lorsque le gestionnaire décide d'exécuter un nouveau thread, il choisira d'abord parmi ceux de plus haute priorité, même si cela peut totalemnt annilhiler les threads de mondre priorité.
Un thread peut être transformé en thread démon en appelant la méthode setDaemon() :
thread.setDeamon(true);
Il n'y a rien de démoniaque là-dedans ! un démon est simplement un thread qui n'a aucun autre but dans la vie que de servir d'autres threads. Nous pouvons citer comme exemples des threads chronomètres qui envoient des "pulsations de temps". régulières à d'autres threads ou à des threads qui nettoient les entrées de cache. Lorsqu'il ne reste plus que les threads démons, la machine virtuelle se ferme. Il n'y a en effet aucun intérêt à continuer d'exécuter un programme si tous les threads restants sont des démons.
Les threads démons sont parfois employés par erreur par les débutants qui ne réfléchissent pas aux actions de fermeture. Cela peut toutefois être dangereux. Un thread démon ne doit jamais accéder à une ressource persistante comme un fichier ou une base de données puisqu'il peut se terminer à tout moment, même au milieu d'une opération.
La méthode run() d'un thread ne peut pas déclencher d'exceptions sous contrôle, mais elle peut être terminée par une exception hors contrôle. Dans ce cas le thread meurt.
Il n'existe toutefois pas de clause catch dans laquelle l'exception peut être propagée. Juste avant que le thread ne meure, l'exception est transférée à un gestionnaire d'exceptions non récupérées.
Depuis Java SE 5.0, vous pouvez installer un gestionnaire dans n'importe quel thread avec la méthode setUncaughtExceptionHandler(). Vous pouvez aussi installer un gestionnaire par défaut pour tous les threads avec la méthode statique setDefaultUncaughtExceptionHandler() de la classe Thread. Un gestionnaire de remplacement pourrait utiliser l'API d'identification pour envoyer des rapports sur les exceptions non récupérées à un fichier journal.
Si vous n'installez pas de gestionnaire par défaut, le gestionnaire par défaut vaut null. Mais, si vous n'installez pas de gestionnaire pour un thread particulier, le gestionnaire correspond à l'objet ThreadGroup du thread.
Un groupe de thread est une collection de thread pouvant être gérés ensemble. Par défaut, tous les threads que vous créez appartiennent au même groupe mais vous pouvez créer d'autres groupements.
Depuis que Java SE 5.0 présentent de meilleures fonstionnalités pour les collections de thread, vous ne devez pas utiliser de groupes de thread dans vos propres programmes.
La classe ThreadGroup implémente l'interface Thread.UncaughtExceptionHandler. Sa méthode uncaughtException() réalise l'action suivante :
C'est une trace que vous avez certainement beaucoup vue dans vos programmes.
§
Toutefois, si erreur est un objet ThreadDeath, la trace est supprimée. Les objets ThreadDeath sont générés par la méthode stop(), aujourd'hui obsolète.
§
Dans la plupart des applications pratiques à base de multithreads, il arrive que plusieurs threads doivent partager un accès aux mêmes objets. Que se passe t-il si deux threads ont accès au même objet et que chacun appelle une méthode qui modifie l'état de cet objet ? comme vous pouvez vous l'imaginer, les threads se font concurrence. Selon l'ordre dans lequel la donnée a été accédée, des objets corrompus peuvent apparaître. Ce type de situation est souvent appelé une condition de course.
Pour éviter que plusieurs threads ne corrompent des données partagées, vous devez apprendre à synchroniser l'accès. Dans cette section nous allons voir ce qui se passe si nous n'utilisons pas de synchronisation.
package synchronisation; public class NonSynchronisé { private static final int NOMBRE_COMPTE = 100; private static final double SOLDE_INITIAL = 1000.0; public static void main(String[] args) { Banque banque = new Banque(NOMBRE_COMPTE, SOLDE_INITIAL); for (int i=0; i<NOMBRE_COMPTE; i++) new Thread(new Transfert(banque, i, SOLDE_INITIAL)).start(); } } class Banque { private final double[] comptes; public Banque(int nombre, double soldeInitiale) { comptes = new double[nombre]; for (int i=0; i<nombre; i++) comptes[i] = soldeInitiale; } public void transfert(int de, int vers, double somme) { if (comptes[de] < somme) return; System.out.print(Thread.currentThread()); comptes[de] -= somme; System.out.printf( , somme, de, vers); comptes[vers] += somme; System.out.printf( , getBalanceTotale()); } public double getBalanceTotale() { double solde = 0.0; for (double compte : comptes) solde += compte; return solde; } public int nombreDeComptes() { return comptes.length; } } class Transfert implements Runnable { private Banque banque; private int duCompte; private double maximumAutorisé; public Transfert(Banque banque, int duCompte, double maximumAutorisé) { this.banque = banque; this.duCompte = duCompte; this.maximumAutorisé = maximumAutorisé; } public void run() { try { int versCompte = (int)(banque.nombreDeComptes() * Math.random()); double montant = maximumAutorisé * Math.random(); banque.transfert(duCompte, versCompte, montant); Thread.sleep((int)(10*Math.random())); } catch (InterruptedException e) {} } }
Le code de cette simulation est assez simple. Il est principalement constitué :
Lorsque cette simulation est exécutée, nous ne savons pas combien d'argent se trouve dans chaque compte bancaire. En revanche, nous savons que la somme de tous les comptes doit rester constante, puisque nous n'effectuons uniquement que des transferts d'argent entre ces comptes (de la même banque). A la fin de chaque transaction, la méthode transfert() recalcule le total et l'affiche.
run:
...
Thread[Thread-57,5,main] 391,83 € de 57 à 60 Balance totale : 100000,00 € Thread[Thread-59,5,main] 954,27 € de 59 à 3 Balance totale : 100000,00 € Thread[Thread-61,5,main] 255,27 € de 61 à 25 Balance totale : 100000,00 € Thread[Thread-63,5,main] 487,74 € de 63 à 25 Balance totale : 100000,00 € Thread[Thread-65,5,main] 204,55 € de 65 à 91 Balance totale : 100000,00 €
... Thread[Thread-10,5,main] 362,20 € de 10 à 61 Balance totale : 99114,95 € Thread[Thread-12,5,main] 320,41 € de 12 à 45 Balance totale : 99114,95 € Thread[Thread-14,5,main] 873,93 € de 14 à 76 Balance totale : 99114,95 € Thread[Thread-16,5,main] 723,88 € de 16 à 1 Balance totale : 99114,95 € ... BUILD SUCCESSFUL (total time: 0 seconds)
Comme nous pouvons le constater, nous obtenons une erreur importante. Pour certaines transactions, le solde reste inférieur à 100 000 €, ce qui correspond normalement au total correct pour 100 compte de 10 000 € chacun. Mais après un certain moment, le solde total est légèrement modifié.
Lorsque vous excécutez ce programme, vous vous rendez compte que des erreurs se produisent rapidement, ou au contraire qu'il faut un certain temps pour que le solde soit corrompu. Cette situation n'inspire pas confiance et vous n'aurez probablement pas envie de déposer votre argent difficilement gagné dans cette banque.
Dans la section précédente, nous avons exécuté un programme dans lequel plusieurs threads mettaient à jour des soldes de comptes banquaires. Après un certain temps, des erreurs apparaissaient et une certaine quantité d'argent était soit perdue, soit ajoutée spontanément.
comptes[vers] += somme;
Le problème de cette instruction est qu'elle n'est pas composée d'opérations atomiques. Cette instruction peut en effet être décomposée comme suit :
Maintenant, supposons que le premier thread exécute les deux premières étapes, puis qu'il soit préempté. Supposons ensuite que le second thread se réveille et qu'il mette à jour la même entrée dans le tableau comptes. Le premier thread se réveille alors et termine sa troisième étape. Cette action annule la modification de l'autre thread. Par conséquent, le total n'est plus correct.
Notre programme de test détecte cette erreur. Naturellement, il existe une légère possibilité que de fausses alarmes se présentent si le thread est interrompu alors qu'il effectue le test !
Quelle est la probabilité que cette erreur se produise ? Nous avons déterminé que nous pouvions augmenter la probabilté d'erreur en entrelaçant les instructions d'affichage à celles qui actualisent le solde.
Si vous oubliez les instructions d'affichage, le risque de corruption diminue un peu car chaque thread travaille très peu avant de se rendormir. Il est donc peu probable que le gestionnaire le préempte au milieu du calcul.
Mais le risque de corruption demeure. Si vous exécutez de très nombreux threads sur une machine très chargée, le programme échouera, même lorsque vous aurez éliminé les instruction d'affichage. Il n'existe rien de pire, ou presque, pour un programmeur qu'une erreur qui ne se manifeste que tous les deux ou trois jours.
Le vrai problème réside dans le fait que le travail de la méthode transfert() peut être interrompu en plein milieu. Si nous pouvions nous assurer que la méthode s'exécute jusqu'à la fin, avant la perte de contrôle du thread, l'état du tableau des comptes en banque ne serait jamais corrompu.
Depuis Java SE 5.0, il existe deux mécanismes permettant de protéger un bloc de code d'un accès simultané :
Le cadre java.util.concurrent propose des classes séparées pour ces mécanismes fondamentaux, que nous expliquerons plus loin dans ce chapitre. Lorsque nous aurons compris ces bases, nous verrons le mot clé synchronised.
verrou.lock(); // un objet ReentrantLock try { // section principale } finally { verrou.unlock(); // vérifier que le verrou est dévérouillé, même si une exception est déclenchée }
Cette construction garantit que seul un thread à la fois puisse entrer dans la section principale. Dès qu'un thread vérouille l'objet verrou, aucun autre thread ne peut entrer dans l'instruction lock(). Lorsque d'autres threads appellent lock(), ils sont bloqués, jusqu'à ce que le premier thread débloque l'objet verrou.
ATTENTION : Il est particulièrement important que l'opération unlock() soit enserrée dans une clause finally. Si le code de la section critique déclenche une exception, le verrou doit être dévérouillé, faute de quoi les autres threads seront bloqués pour toujours.
class Banque { private final double[] comptes; private Lock verrou = new ReentrantLock(); // ReentrantLock implémente l'interface Lock ... public void transfert(int de, int vers, double somme) { verrou.lock(); System.out.print(Thread.currentThread()); comptes[de] -= somme; System.out.printf( , somme, de, vers); comptes[vers] += somme; System.out.printf( , getBalanceTotale()); verrou.unlock(); } ...
Supposons qu'un thread appelle la méthode transfert() et soit préempté avant d'avoir terminé. Supposons ensuite qu'un deuxième thread appelle aussi la méthode transfert(). Le deuxième ne peut pas acquérir le verrou, il est bloqué dans l'appel de la méthode lock(). Il est donc désactivé et doit attendre que le premier thread finisse d'exécuter la méthode tranfert(). Lorsque le premier thread débloque le verrou au travers de la méthode unlock(), le deuxième peut alors continuer.
run:
...
Thread[Thread-57,5,main] 391,83 € de 57 à 60 Balance totale : 100000,00 € Thread[Thread-59,5,main] 954,27 € de 59 à 3 Balance totale : 100000,00 € Thread[Thread-61,5,main] 255,27 € de 61 à 25 Balance totale : 100000,00 € Thread[Thread-63,5,main] 487,74 € de 63 à 25 Balance totale : 100000,00 € Thread[Thread-65,5,main] 204,55 € de 65 à 91 Balance totale : 100000,00 €
... Thread[Thread-10,5,main] 362,20 € de 10 à 61 Balance totale : 100000,00 € Thread[Thread-12,5,main] 320,41 € de 12 à 45 Balance totale : 100000,00 € Thread[Thread-14,5,main] 873,93 € de 14 à 76 Balance totale : 100000,00 € Thread[Thread-16,5,main] 723,88 € de 16 à 1 Balance totale : 100000,00 € ... BUILD SUCCESSFUL (total time: 0 seconds)
Vous remarquez que chaque objet banque possède son propre objet ReentrantLock. Si deux threads tentent d'accéder au même objet banque, le verrou sert à sérialiser l'accès. Toutefois, si deux threads tentent d'accéder à différents objets banque, chaque thread acquiert un verrou différent et aucun thread n'est bloqué. Ce fonctionnement est souhaitable, car les threads ne doivent pas interférer les uns avec les autres lorsqu'ils manipulent différentes instances de Banque.
Le verrou est dit rentrant car un thread peut acquérir, à plusieurs reprises, un verrou qu'il possède déjà. Le verrou tient le compte des appels imbriqués à la méthode lock(). Le thread doit appeler unlock() pour chaque appel à lock(), de manière à renoncer au verrou. Du fait de cette caractéristique, le code protégé par un verrou peut appeler une autre méthode qui utilise les mêmes verrous.
Si, par exemple, la méthode transfert() appelle la méthode getBalanceTotale(), celle-ci vérouille aussi l'objet verrou, lequel affiche maintenant un compte de 2. Lorsque la méthode getBalanceTotale() se termine, le compte revient à 1. Â la fin de la méthode transfert(), le compte est à 0 et le thread renonce au verrou.
En général, il est souhaitable de protéger les blocs de code qui mettent à jour ou inspectent un objet partagé. Vous êtes alors assuré que ces opérations s'exécutent jusqu'au bout avant qu'un autre thread ne puisse utiliser le même objet.
Attention : Vous devez prendre garde à ce que le code d'une section principale ne soit pas contourné par le déclanchement d'une exception. Si une exception est déclenchée avant la fin de la section, la clause finally renoncera au verrou, mais l'objet pourra être endommagé.
Même si l'équité paraît adaptée, ces verrous sont bien plus lents que les verrous ordinaires. Activez-les seulement si vous voulez véritablement savoir ce que vous faites et si vous avez une bonne raison de les employer. Même si vous utilisez un verrou équitable, vous n'avez aucune garantie que son gestionnaire le soit. S'il choisit de négliger un thread qui attend le verrou depuis longtemps, il n'aura pas l'occasion d'être traité équitablement pas le verrou.
Très souvent, un thread entre dans une section principale, uniquement pour découvrir qu'il ne peut pas poursuivre tant qu'une condition n'est pas remplie. Vous utiliserez alors un objet de condition pour gérer les threads qui ont acquis un verrou mais ne peuvent pas procéder à un travail utile.
if (banque.getBalance(duCompte) >= montant) banque.transfert(duCompte, auCompte, montant);
if (banque.getBalance(duCompte) >= montant) // le thread risque d'être désactivé ici banque.transfert(duCompte, auCompte, montant);
class Banque { private final double[] comptes; private Lock verrou = new ReentrantLock(); ... public void transfert(int de, int vers, double somme) { verrou.lock(); while (compte[de] < somme) { System.out.print(Thread.currentThread()); comptes[de] -= somme; System.out.printf( , somme, de, vers); comptes[vers] += somme; System.out.printf( , getBalanceTotale()); } verrou.unlock(); } ...
Que faire maintenant lorsqu'il n'y a pas suffisamment d'argent sur le compte ? Nous attendons qu'un autre thread ajoute des fonds. Mais ce thread vient d'obtenir un accès exclusif à verrou, aucun autre thread n'a donc l'occasion de réaliser un dépôt. C'est ici que les objets de condition entrent en jeu.
class Banque { private final double[] comptes; private Lock verrou = new ReentrantLock(); private Condition fondsSuffisants = verrou.newCondition(); ...
fondsSuffisants.await();Le thread actuel est maintenant désactivé et abandonne le verrou, ce qui permet à un autre thread d'entrer et, nous l'espérons, augmenter le solde.
Il existe une différence essentielle entre un thread qui attend d'acquérir un verrou et un thread qui a appelé la méthode await(). Lorsqu'un thread appelle la méthode await(), il entre un jeu d'attente pour cette condition. Le thread n'est pas rendu exécutable lorsque le verrou est disponible. Il reste désactivé jusqu'à ce qu'un autre thread appelle la méthode signalAll() sur la même condition.
fondsSuffisants.signalAll();
Cet appel réactive tous les threads qui attendent la condition. Lorsque les threads sont retirés du jeu d'attente, ils redeviennent exécutables et le gestionnaire finira par les réactiver. A ce moment là, ils tenterons de rentrer dans l'objet. Dès que le verrou est diponible, l'un d'entre eux acquerra le verrou et continuera là ou il s'est arrêté, en revenant à l'appel await().
A ce moment là, le thread doit à nouveau tester la condition. Il n'est pas garanti que la condition soit remplie. La méthode signalAll() signale simplement aux threads en attente qu'elle peut être remplie à ce moment là et quil faut à nouveau vérifier la condition.
En général, un appel à la méthode await() doit se trouver dans une boucle sous la forme :
while (!(ok pour continuer)) fondsSuffisants.await();
Il est particulièrement important qu'un seul thread appelle la méthode signalAll(). Lorsqu'un thread appelle la méthode await(), il n'a aucun moyen de le réactiver. Il met sa confiance dans les autres threads. Si aucun d'entre eux ne se préoccupe de réactiver le thread en attente, il ne sera jamais réexécuté. Cela peut mener à des situations déplaisantes de verrous morts. Si tous les autres threads sont bloqués et que le dernier thread actif appelle await() sans débloquer l'un des autres, il se bloque aussi. Aucun thread n'ayant l'autorisation de débloquer les autres, le programme plante.
A quel moment appeler signalAll() ? En règle générale, il vaut mieux l'appeler dès que l'état d'un objet change d'une manière qui pourrait être avantageuse pour les threads qui sont en attente. Par exemple, dès qu'un solde banquaire change, les threads en attente doivent avoir une occasion d'inspecter le solde.
class Banque { private final double[] comptes; private Lock verrou = new ReentrantLock(); private Condition fondsSuffisants = verrou.newCondition(); ... public void transfert(int de, int vers, double somme) { verrou.lock(); while (compte[de] < somme) fondsSuffisants.await(); System.out.print(Thread.currentThread()); comptes[de] -= somme; System.out.printf( , somme, de, vers); comptes[vers] += somme; System.out.printf( , getBalanceTotale()); fondsSuffisants.signalAll(); verrou.unlock(); } ...Vous remarquez que l'appel à signalAll() n'active pas immédiatement un thread en attente. Il ne fait que débloquer les threads en attente de sorte qu'ils puissent entrer en concurrence pour entrer dans l'objet après que le thread actuel est sortie de la méthode synchronisée
Une autre méthode, signal(), ne débloque qu'un thread choisi au hasard dans le jeu d'attente. Ceci est plus efficace que débloquer tous les threads, mais il y a un danger. Si le thread choisi aléatoirement découvre qu'il ne peut toujours pas poursuivre, il est à nouveau bloqué. Si aucun thread n'appelle signal(), le système génère des verrous morts.
package synchronisation; import java.util.concurrent.locks.*; public class Synchronisé { private static final int NOMBRE_COMPTE = 100; private static final double SOLDE_INITIAL = 1000.0; public static void main(String[] args) { Banque banque = new Banque(NOMBRE_COMPTE, SOLDE_INITIAL); for (int i=0; i<NOMBRE_COMPTE; i++) new Thread(new Transfert(banque, i, SOLDE_INITIAL)).start(); } } class Banque { private final double[] comptes; private Lock verrou = new ReentrantLock(); private Condition fondsSuffisants = verrou.newCondition(); public Banque(int nombre, double soldeInitiale) { comptes = new double[nombre]; for (int i=0; i<nombre; i++) comptes[i] = soldeInitiale; } public void transfert(int de, int vers, double somme) throws InterruptedException { verrou.lock(); try { while (comptes[de] < somme) fondsSuffisants.await(); System.out.print(Thread.currentThread()); comptes[de] -= somme; System.out.printf( , somme, de, vers); comptes[vers] += somme; System.out.printf( , getBalanceTotale()); fondsSuffisants.signalAll(); } finally { verrou.unlock(); } } public double getBalanceTotale() { verrou.lock(); try { double solde = 0.0; for (double compte : comptes) solde += compte; return solde; } finally { verrou.unlock(); } } public int nombreDeComptes() { return comptes.length; } } class Transfert implements Runnable { private Banque banque; private int duCompte; private double maximumAutorisé; public Transfert(Banque banque, int duCompte, double maximumAutorisé) { this.banque = banque; this.duCompte = duCompte; this.maximumAutorisé = maximumAutorisé; } public void run() { try { int versCompte = (int)(banque.nombreDeComptes() * Math.random()); double montant = maximumAutorisé * Math.random(); banque.transfert(duCompte, versCompte, montant); Thread.sleep((int)(10*Math.random())); } catch (InterruptedException e) {} } }
Dans la pratique, une utilisation correcte des conditions peut être assez difficile. Avant de lancer l'implémentation de vos propres objets de condition, envisagez l'une des constructions décrites plus loin, dans la section sur la synchronisation.
Dans les sections précédentes, vous avez vu comment utiliser Lock et les objets Condition. Avant de poursuivre, résumons les points principaux concernant les verrous et les conditions :
Les interfaces Lock et Condition ont été ajoutées à Java SE 5.0 pour donner aux programmeurs un meilleur contrôle sur le vérouillage. Toutefois, la plupart du temps, ce contrôle est inutile et vous pouvez utiliser un mécanisme directement intégré dans le langage Java. En effet, depuis la version 1.0, chaque objet de Java possède un verrou intrinsèque. Si une méthode est déclarée avec le mot clé synchroniszed, le verrou d'objet protède toute la méthode. Pour appeler la méthode, un thread doit donc acquérir le verrou de l'objet intrinsèque.
public synchronized void uneMéthode() { // section principale }équivaut à :
public void uneMéthode() { verrou.lock(); // un objet ReentrantLock try { // section principale } finally { verrou.unlock(); // vérifier que le verrou est dévérouillé, même si une exception est déclenchée } }
Par exemple, au lieu d'utiliser un verrou explicite, nous pouvons simplement déclarer comme synchronized la méthode transfert() de la classe Banque.
Le verrou d'objet intrinsèque ne s'est vu associer qu'une seule condition. La méthode wait() ajoute un thread au jeu d'attente et les méthodes notifyAll() et notify() débloquent les threads en attente.
Autrement dit, appelez wait() et notifyAll() équivaut respectivement à :
fondsSuffisants.await(); fondsSuffisants.signalAll();
Les méthodes wait() et notifyAll() ou notify() sont des méthodes final de la classe Object. Les méthodes de Condition ont dû être nommées await(), signal() et signalAll() pour ne pas rentrer en confilt avec les précédentes.
class Banque { private final double[] comptes; ... public synchronized void transfert(int de, int vers, double somme) { while (compte[de] < somme) wait(); // attend la seule condition du verrou de l'objet intrinsèque System.out.print(Thread.currentThread()); comptes[de] -= somme; System.out.printf( , somme, de, vers); comptes[vers] += somme; System.out.printf( , getBalanceTotale()); notifyAll(); // avertir tous les threads attendant la condition } ...
Comme vous le voyez, l'utilisation du mot clé synchronized permet d'avoir un code bien plus concis. Pour le comprendre, vous devez savoir que chaque objet possède un verrou intrinsèque et que le verrou a une condition intrinsèque. Le verrou gère les threads qui tentent d'entrer dans une méthode synchronized. La condition gère les threads qui ont appelée la méthode wait().
Vous pouvez aussi déclarer une méthode statique comme synchronisée. Lorsque cette méthode est appelée, elle acquiert l'aspect intrinsèque de l'objet de classe associée. Par exemple, si la classe Banque possède une méthode synchronisée statique, le verrou de l'objet Banque.class est vérouillé lorsqu'il est appelé. En conséquence, aucun autre thread ne peut appeler cette méthode statique synchronisée de la même classe ou une autre.
Les verrous intrinsèques et les conditions présentent certaines limites, et parmi elles :
Nous pouvons alors nous interroger sur ce qu'il faut utiliser dans le code : des objets Lock et Condition ou des méthodes synchonisées ? Voici quelques conseils :
package synchronisation; public class Synchronisé { private static final int NOMBRE_COMPTE = 100; private static final double SOLDE_INITIAL = 1000.0; public static void main(String[] args) { Banque banque = new Banque(NOMBRE_COMPTE, SOLDE_INITIAL); for (int i=0; i<NOMBRE_COMPTE; i++) new Thread(new Transfert(banque, i, SOLDE_INITIAL)).start(); } } class Banque { private final double[] comptes; public Banque(int nombre, double soldeInitiale) { comptes = new double[nombre]; for (int i=0; i<nombre; i++) comptes[i] = soldeInitiale; } public synchronized void transfert(int de, int vers, double somme) throws InterruptedException { while (comptes[de] < somme) wait(); System.out.print(Thread.currentThread()); comptes[de] -= somme; System.out.printf( , somme, de, vers); comptes[vers] += somme; System.out.printf( , getBalanceTotale()); notifyAll(); } public synchronized double getBalanceTotale() { double solde = 0.0; for (double compte : comptes) solde += compte; return solde; } public int nombreDeComptes() { return comptes.length; } } class Transfert implements Runnable { private Banque banque; private int duCompte; private double maximumAutorisé; public Transfert(Banque banque, int duCompte, double maximumAutorisé) { this.banque = banque; this.duCompte = duCompte; this.maximumAutorisé = maximumAutorisé; } public void run() { try { int versCompte = (int)(banque.nombreDeComptes() * Math.random()); double montant = maximumAutorisé * Math.random(); banque.transfert(duCompte, versCompte, montant); Thread.sleep((int)(10*Math.random())); } catch (InterruptedException e) {} } }
Nous venons de le voir, chaque objet Java possède un verrou en appelant une méthode synchronisée. Il existe un second mécanisme qui réalise le même type de service, en utilisant cette fois-ci le bloc synchronisé.
synchronized (objet) { // syntaxe pour un bloc synchonisé // section principale }
Vous trouverez souvent des verrous ad hoc dans un code existant, comme :
class Banque { private final double[] comptes; private Object verrou = new Object(); ... public void transfert(int de, int vers, double somme) { ... synchronized (verrou) { comptes[de] -= somme; comptes[vers] += somme; } ... }
Ici, l'objet verrou n'est créé que pour utiliser le verrou que possède chaque objet Java.
.
Les verrous et les conditions constituent des outils puissants pour la synchronisation des threads, mais ils ne sont pas vraiment orientés objet. L'une des solutions les plus importantes est le concept de moniteur.
En terminologie Java, un moniteur présente les propriétés suivantes :
Les concepteurs de Java ont adapté le concept du moniteur. Chaque objet de Java possède un verrou intrinsèque et une condition intrinsèque. Si une méthode est déclarée avec le mot réservée synchronized, elle agit comme une méthode moniteur. La variable de condition est accessible par un appel à wait()/notify()/notifyAll().
Toutefois, un objet Java diffère d'un moniteur en trois points importants, qui compromettent la sécurité des threads :
Un thread se bloque de manière indéfinie lorsqu'il appelle la méthode lock() pour acquérir un verrou appartenant à un autre thread. Vous pouvez prendre des précautions supplémentaires pour acquérir un verrou.
public void uneMéthode() { if (verrou.tryLock()) { // le thread possède alors le verrou try { ... } finally { verrou.unlock(); } else // faire autre chose }Vous pouvez également appelez la méthode tryLock() avec un paramètre de temporisation, comme ceci :
public void uneMéthode() { if (verrou.tryLock(100, TimeUnit.MILLISECONDS)) { // le thread possède alors le verrou try { ... } finally { verrou.unlock(); } else // faire autre chose }
TimeUnit est une énumération ayant les valeurs SECONDS, MILLISECONDS, MICROSECONDS et NANOSECONDS.
§
La méthode lock() ne peut pas être interrompue. Si un thread est interrompu lorsqu'il attend d'acquérir un verrou, il reste bloqué jusqu'à ce que le verrou soit disponible. En cas de verrou mort, la méthode lock() ne peut jamais se terminer.
Toutefois, si vous appelez tryLock() avec une temporisation, une InterruptedException est déclenchée si le thread est interrompu pendant qu'il attend. C'est particulièrement utile car cela permet à un programme de casser les verrous morts.
Il existe d'autres méthodes qui permettent de prendre en compte la notion de temps :
condition.await(100, TimeUnit.MILLISECONDS);
La méthode await() se termine si un autre thread a activé ce thread en appelant signalAll() ou signal() ou si la temporisation s'est achevée ou si le thread a été interrompu.
Les méthodes await() déclenchent une InterruptedException si le thread en attente est interrompu.
§
Le paquetage java.util.concurrent.locks définit deux classes de verrou, ReentranLock, que nous connaissons déjà, et la classe ReentrantReadWriterLock. Cette dernière sert lorsque trop de thread lisent à partir d'une structure de données et qu'il existe moins de threads pour les modifier.
Dans cette situation, il est logique d'autoriser l'accès pour les lecteurs. Bien entendu, un système d'écriture doit toujours avoir un accès exclusif.
§
Voici les étapes nécessaires pour utiliser la lecture et l'écriture de verrous :
class Banque { ... private ReentrantReadWriteLock verrou = new ReentrantReadWriteLock(); ...
class Banque { ... private ReentrantReadWriteLock verrou = new ReentrantReadWriteLock(); private Lock verrouEnLecture = verrou.readLock(); private Lock verrouEnEcriture = verrou.writeLock(); ...
... public double getBalanceTotale() { verrouEnLecture.lock(); try { ... } finally { verrouEnLecture.unlock(); } } ...
... public void transfert(int de, int vers, double somme) { verrouEnEcriture.lock(); try { ... } finally { verrouEnEcriture.unlock(); } } ...
package synchronisation; import java.util.concurrent.locks.*; public class Synchronisé { private static final int NOMBRE_COMPTE = 100; private static final double SOLDE_INITIAL = 1000.0; public static void main(String[] args) { Banque banque = new Banque(NOMBRE_COMPTE, SOLDE_INITIAL); for (int i=0; i<NOMBRE_COMPTE; i++) new Thread(new Transfert(banque, i, SOLDE_INITIAL)).start(); } } class Banque { private final double[] comptes; private ReentrantReadWriteLock verrou = new ReentrantReadWriteLock(); private Lock verrouEnLecture =verrou.readLock(); private Lock verrouEnEcriture =verrou.writeLock(); private Condition fondsSuffisants = verrouEnEcriture.newCondition(); public Banque(int nombre, double soldeInitiale) { comptes = new double[nombre]; for (int i=0; i<nombre; i++) comptes[i] = soldeInitiale; } public void transfert(int de, int vers, double somme) throws InterruptedException { verrouEnEcriture.lock(); try { while (comptes[de] < somme) fondsSuffisants.await(); System.out.print(Thread.currentThread()); comptes[de] -= somme; System.out.printf( , somme, de, vers); comptes[vers] += somme; System.out.printf( , getBalanceTotale()); fondsSuffisants.signalAll(); } finally { verrouEnEcriture.unlock(); } } public double getBalanceTotale() { verrouEnLecture.lock(); try { double solde = 0.0; for (double compte : comptes) solde += compte; return solde; } finally { verrouEnLecture.unlock(); } } public int nombreDeComptes() { return comptes.length; } } class Transfert implements Runnable { private Banque banque; private int duCompte; private double maximumAutorisé; public Transfert(Banque banque, int duCompte, double maximumAutorisé) { this.banque = banque; this.duCompte = duCompte; this.maximumAutorisé = maximumAutorisé; } public void run() { try { int versCompte = (int)(banque.nombreDeComptes() * Math.random()); double montant = maximumAutorisé * Math.random(); banque.transfert(duCompte, versCompte, montant); Thread.sleep((int)(10*Math.random())); } catch (InterruptedException e) {} } }
La première version de Java a défini une méthode stop() qui se contente de mettre fin à un thread et une méthode suspend() qui bloque un thread jusqu'à ce qu'un autre thread appelle resume(). Ces deux méthodes ont quelque chose en commun : elles tentent de contrôler le comportement d'un thread donné sans sa coopération.
Ces deux méthodes ne sont plus utilisées depuis Java SE 1.2. La méthode stop() est intrinsèquement non sûre et l'expérience a montré que la méthode suspend() amène souvent à des situations de verrous morts. Dans cette section, nous allons voir pourquoi ces méthodes posent des problèmes, et ce que nous pouvons faire pour les éviter.
Cette méthode met fin à toutes les méthodes en attente, y compris la méthode run(). Lorsqu'un thread est arrêté, il rend immédiatement les verrous sur tous les objets qu'il a vérouillés. Cela peut laisser les objets dans un état incohérent.
Par exemple, supposons qu'un thread de transfert soit arrêté en plein milieu d'un déplacement d'argent d'un compte vers un autre, après le retrait et avant le dépôt. L'objet de banque est alors endommagé. Le verrou ayant été abandonné, les dommages peuvent être constatés sur les autres threads qui n'ont pas été arrêtés.
Lorsqu'un thread veut arrêter un autre thread, il n'a aucun moyen de savoir à quel moment l'emploi de la méthode stop() est sûr, et à quel moment il risque d'endommager des objets. Par conséquent cette méthode n'est plus utilisée. Il est conseillé d'interrompre un thread que lorsque vous voulez l'arrêter. Le thread interrompu peut alors s'arrêter au moment opportun.
Contrairement à stop(), la méthode suspend() ne peut pas endommager les objets. Cependant, si vous suspendez un thread qui possède un verrou sur un objet, cet objet ne sera plus disponible jusqu'à ce que le thread ait repris son activité.
Si le thread qui appelle la méthode suspend() essaie d'obtenir le verrou sur le même objet, le programme se bloque : le thread interrompu attend qu'on lui demande de reprendre son activité et le thread qu'il a mis en attente attend que l'objet soit dévérouillé.
boutonPause.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent événement) { for (int i=0; i<threads.lenght; i++) threads[i].suspend() // Surtout ne pas faire cela } }); boutonContinu.addActionListener(...); // appelle resume() sur tous les threads de transfert
Une méthode paintComponent() dessine un graphique pour chaque compte, en appelant la méthode getBalanceTotale().
.
Comme nous le verrons plus tard, les actions des boutons et l'action graphique se trouve dans le même thread, appelé thread de répartition des événements.
Considérons maintenant le scénario suivant :
Le programme est alors bloqué : Le thread de répartition des événements ne peut pas continuer parce que ce verrou appartient à l'un des threads suspendus. Par conséquent, l'utilisateur ne peut pas cliquer sur le bouton "Continu", et aucun thread ne continuera son exécution.
public void run() { while (...) ... if (demandeArrêt) { verrou.lock(); try { while (demandeArrêt) condition.await(); } finally { verrou.unlock(); } } } } private boolean demandeArrêt = false; private Lock verrou = new ReentrantLock(); private Condition condition = verrou.newCondition();
Nous venons de voir les blocs de base de la programmation simultanée en Java. Toutefois, pour la programmation pratique, tenez-vous-en éloigné chaque fois que possible. Il est bien plus facile et sûr d'utiliser des structures de niveau supérieur implémentées par des experts de la simultanéité.
De nombreux problèmes de thread peuvent être formulés de manière élégante et sûre à l'aide d'une ou de plusieurs queues. Les threads de producteurs insèrent des éléments dans la queue, puis les threads de consommation les récupèrent. La queue permet de transférer en toute sécurité des données d'un thread à l'autre.
Etudions, par exemple, notre programme de virement bancaire. Plutôt que d'accéder à l'objet banque directement, les threads de transfert insère des objets d'instruction dans une queue. Un autre thread supprime les instructions de la queue et réalise les transferts.
Seul ce thread a accès aux coulisses de l'objet banque. Aucune synchronisation n'est nécessaire (bien entendu, les implémenteurs des classes de queue compatibles avec les threads ont dû s'inquiéter des verrous et des conditions, mais c'était leur problème, ne vous en préoccuper pas).
Une queue de blocage amène un thread à se bloquer lorsque vous tentez d'ajouter un élément alors que la queue est pleine ou de supprimer un élément alors qu'elle est vide. Les queues de blocages sont un outil utile pour coordonner le travail de plusieurs threads. Des threads de travail suppriment les résultats intermédiaires et les modifient ensuite. La queue équilibre automatiquement la charge de travail.
Si le premier ensemble de threads s'exécute plus lentement que le second, celui-ci se bloque en attendant les résultats. Si le premier jeu de thread s'exécute plus rapidement, la queue se remplit jusqu'à ce que le second ensemble rattrape son retard.
Voici la liste des méthodes relatives aux queues de blocage :
Les méthodes poll() et peek() renvoient null pour signaler une erreur. L'insertion de valeurs null dans ces queues n'est donc pas autorisée.
§
Le fonctionnement des queues de blocage est réparti en trois catégories, selon leur action lorsque la queue est pleine ou vide :
boolean succès = queue.offer(x, 100, TimeUnit.MILLISECONDS); Object tête = queue.poll(100, TimeUnit.MILLISECONDS);
La méthode put() bloque si la queue est pleine, la méthode take() bloque si la queue est vide. Ce sont les équivalents d'offer() et poll(), sans temporisation.
§
Le paquetage java.util.concurrent propose plusieurs variations des queues de blocage :
Si l'équité est spécifiée, les threads qui ont attendu le plus longtemps reçoivent un traitement préférentiel. Comme toujours, l'équité pénalise considérablement les performances (ne l'utilisez que si vos problèmes l'exigent spécifiquement).
Revoir l'étude sur les collections pour en savoir plus sur les queues de priorité.
§
interface Delayed extends Comparable<Delayed> { long getDelay(TimeUnit unité); }
Construisent une queue ou deque de blocage avec la capacité donnée, implémentées sous forme de liste chaînée.
Le programme ci-dessous montre l'utilisation d'une queue de blocage qui contrôle un ensemble de threads. Le programme cherche dans les fichiers d'un répertoire et de ses sous-répertoire et affiche les lignes qui contiennent le mot spécifié.
Sachez que la synchronisation explicite des threads n'est pas nécessaire. Dans cette application, nous utilisons la structure de données de la queue comme mécanisme de synchronisation.
package synchronisation; import java.io.*; import java.util.*; import java.util.concurrent.*; public class QueueDeBlocage { private static final int TAILLE_QUEUE = 10; private static final int THREADS_DE_RECHERCHE = 100; private static String répertoireDeBase = ; public static void main(String[] args) { Scanner clavier = new Scanner(System.in); System.out.print( ); String motDeRecherche = clavier.nextLine(); BlockingQueue<File> queue = new ArrayBlockingQueue<File>(TAILLE_QUEUE); EnumérationFichiers fichiers = new EnumérationFichiers(queue, new File(répertoireDeBase)); new Thread(fichiers).start(); for (int i = 1; i <= THREADS_DE_RECHERCHE; i++) new Thread(new TâcheDeRecherche(queue, motDeRecherche)).start(); } } class EnumérationFichiers implements Runnable { private BlockingQueue<File> queue; private File répertoire; public static File VIERGE = new File( ); public EnumérationFichiers(BlockingQueue<File> queue, File répertoire) { this.queue = queue; this.répertoire = répertoire; } public void run() { try { énumère(répertoire); queue.put(VIERGE); } catch (InterruptedException e) {} } public void énumère(File répertoire) throws InterruptedException { File[] fichiers = répertoire.listFiles(); for (File fichier : fichiers) { if (fichier.isDirectory()) énumère(fichier); else { queue.put(fichier); System.out.printf(\n , fichier.getPath()); } } } } class TâcheDeRecherche implements Runnable { private BlockingQueue<File> queue; private String mot; public TâcheDeRecherche(BlockingQueue<File> queue, String mot) { this.queue = queue; this.mot = mot; } public void run() { try { boolean fini = false; while (!fini) { File fichier = queue.take(); if (fichier == EnumérationFichiers.VIERGE) { queue.put(fichier); fini = true; } else recherche(fichier); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { } } public void recherche(File fichier) throws IOException { Scanner lecture = new Scanner(new FileInputStream(fichier)); int nombreLigne = 0; while (lecture.hasNextLine()) { nombreLigne++; String ligne = lecture.nextLine(); if (ligne.contains(mot)) System.out.printf( , fichier.getPath(), nombreLigne, ligne); } lecture.close(); } }
Si plusieurs threads modifient simultanément une structure de données, telle qu'une table de hachage, cette structure de données peut facilement être endommagée.
Un thread pourrait, par exemple, commencer à insérer un nouvel élément. Supposons qu'il soit alors préempté au milieu du réacheminement des liens entre les seaux de la table de hachage. Si un autre thread commence à parcourir la même liste, il risque de suivre des liens non valides et de créer un déséquilibre, par exemple en lançant des exceptions ou en s'engageant dans une boucle sans fin.
Pour en savoir plus sur les seaux et les tables de hachage.
§
Vous pouvez protéger une structure de données partagées en fournissant un verrou, mais il est généralement plus simple de choisir une implémentation compatible avec les threads. Les queues de blocages vues précédemment sont, bien entendu, des collections compatibles avec les threads. Dans cette section, nous en verrons d'autres proposées par la bibliothèque Java.
Le paquetage java.util.concurrent apporte des implémentations efficaces pour les cartes, les jeux triés et les queues :
Ces collections utilisent des algorithmes complexes qui réduisent la contention en permettant un accès simultané à plusieurs parties de la structure de données.
A la différence de la plupart des collections, la méthode size() ne fonctionne pas forcément en temps constant. Déterminer la taille actuelle de l'une de ces collections nécessite généralement de les parcourir.
Au contraire, un itérateur d'une collection du paquetage java.util déclenche une ConcurrentModificationException lorsque la collection a été modifié après la construction de l'itérateur.
La table de hachage simultanée peut efficacement supporter plusieurs lecteurs et un nombre de système d'écriture fixe. Par défaut, il peut exister jusqu'à 16 threads d'écriture simultanés. Nous pouvons avoir plus de threads d'écriture mais si plus de seize écrivent en même temps, les autres sont temporairement bloqués. Vous pouvez spécifier un nombre supérieur dans le constructeur mais il est certainement peu probable que vous deviez le faire.
Les classes ConcurrentHashMap et ConcurrentSkipListMap possèdent des méthodes utiles pour des associations atomiques d'insertion et de retrait :
cache.putIfAbsent(clé, valeur);
cache.remove(clé, valeur);
cache.replace(clé, ancienneValeur, nouvelleValeur);
CopyOnWriteArray et CopyOnWriteArraySet sont des collections compatibles avec les threads, dans lesquelles toutes les méthodes de modification créent une copie du tableau sous-jacent. Cet arrangement est utile lorsque le nombre de threads à parcourir la collection dépasse de loin le nombre de ceux qui la modifient.
Lorsque vous construisez un itérateur, il contient une référence au tableau actuel. Si le tableau est modifié par la suite, l'itérateur possède toujours l'ancien tableau, mais celui de la collection est remplacé. Par conséquent, l'itérateur le plus ancien possède une vue cohérente (mais peut-être obsolète) à laquelle il peut accéder sans frais de synchronisation.
Un Runnable encapsule une tâche qui s'exécute de manière asynchrone. C'est en quelque sorte une méthode asynchrone sans paramètres ni valeur de retour.
Un Callable est identique, mais il renvoie une valeur. L'interface Callable est un type à paramètres, avec une seule méthode call() :
public interface Callable<Value> { Value call() throws Exception; }
Le paramètre de type correspond au type de la valeur renvoyée. Par exemple Callable<Integer> représente un calcul asynchrone qui finit par renvoyer un entier (objet Integer).
Un Future contient le résultat d'un calcul asynchrone. Un Future permet de démarrer un calcul, de donner le résultat à quelqu'un, puis de l'oublier. Lorsqu'il est prêt, le propriétaire de l'objet Future peut obtenir le résultat.
L'interface Future possède les méthodes suivantes :
public interface Future<Value> { Value get() throws ...; Value get(long durée, TimeUnit unitéDeTemps) throws ...; boolean cancel(Boolean peutÊtreInterrompu); boolean isCancelled(); boolean isDone(); }
L'emballage FutureTask est un mécanisme commode pour transformer un Callable à la fois en Future et en Runnable : il implémente les deux interfaces. Par exemple :
Callable<Integer> commutateur = ...; FutureTask<Integer> question = new FutureTask<Integer>(commutateur); Thread tâche = new Thread(question); // C'est un Runnable tâche.start(); ... int résultat = question.get(); // C'est un Future
Le programme ci-dessous illustre ces concepts. Ce programme est identique à l'exemple du chapitre précédent qui trouvait les fichiers contenant un mot clé. Toutefois, nous allons maintenant simplement compter le nombre de fichiers correspondants.
class TâcheDeRecherche implements Callable<Integer> { public TâcheDeRecherche(File répertoire, String mot) { ... } public Integer call() { ... } // Renvoie le nombre de fichiers concordants }
TâcheDeRecherche rechercher = new TâcheDeRecherche(new File(répertoire), mot); FutureTask<Integer> question = new FutureTask<Integer>(rechercher); Thread tâche = new Thread(question); tâche.start();
System.out.println(question.get() + );
Bien entendu, l'appel à get() bloque jusqu'à ce que le résultat soit disponible.
§
for (Future<Integer> résultat : résultats) compteur += résultat.get();
package synchronisation; import java.io.*; import java.util.*; import java.util.concurrent.*; public class PoolDeThreads { private static String répertoire = ; public static void main(String[] args) { Scanner clavier = new Scanner(System.in); System.out.print( ); String mot = clavier.nextLine(); TâcheDeRecherche rechercher = new TâcheDeRecherche(new File(répertoire), mot); FutureTask<Integer> question = new FutureTask<Integer>(rechercher); Thread tâche = new Thread(question); tâche.start(); try { System.out.println(question.get() + ); } catch (ExecutionException e) { e.printStackTrace(); } catch (InterruptedException e) { } } } class TâcheDeRecherche implements Callable<Integer> { private File répertoire; private String mot; private int compteur; public TâcheDeRecherche(File répertoire, String mot) { this.répertoire = répertoire; this.mot = mot; } public Integer call() { compteur = 0; try { File[] fichiers = répertoire.listFiles(); ArrayList<Future<Integer>> résultats = new ArrayList<Future<Integer>>(); for (File fichier : fichiers) if (fichier.isDirectory()) { TâcheDeRecherche rechercher = new TâcheDeRecherche(fichier, mot); FutureTask<Integer> question = new FutureTask<Integer>(rechercher); résultats.add(question); Thread tâche = new Thread(question); tâche.start(); } else if (recherche(fichier)) compteur++; for (Future<Integer> résultat : résultats) try { compteur += résultat.get(); } catch (ExecutionException e) { e.printStackTrace(); } } catch (InterruptedException e) { } return compteur; } public boolean recherche(File fichier) { try { Scanner lecture = new Scanner(new FileInputStream(fichier)); boolean trouvé = false; while (!trouvé && lecture.hasNextLine()) { String ligne = lecture.nextLine(); if (ligne.contains(mot)) trouvé = true; } lecture.close(); return trouvé; } catch (IOException e) { return false; } } }
La construction d'un nouveau thread est assez onéreuse car elle implique une interaction avec le système d'exploitation. Si votre programme crée un grand nombre de threads courts, il devra plutôt utiliser un pool de threads.
Ce pool contient plusieurs threads inactifs, prêts à s'exécuter. Vous attribuez un Runnable au pool et l'un des threads appelle la méthode run(). A la fin de la méthode run(), le thread ne meurt pas, il demeure pour répondre à la requête suivante.
Il existe une autre raison pour utiliser un pool de threads : l'étranglement de plusieurs threads simultanés. La création d'un grand nombre de threads peut considérablement dégrader les performances, voire bloquer la machine virtuelle. Pour un algorithme créateur de nombreux threads, utilisez un pool de threads "fixe" qui limite le nombre total de threads simultanés.
La classe Executors possède plusieurs méthodes de fabrique statiques destinées à la création de pools de threads :
Méthode de fabrique | Description |
---|---|
newCachedThreadPool() | Les nouveaux threads sont créés en fonction des besoins. Les threads inactifs sont conservés pendant 60 secondes. |
newFixedThreadPool() | Le pool contient un jeu fixe de threads. Les threads inactifs sont conservés indéfiniment. |
newSingleThreadExecutor() | Un "pool" possédant un seul thread qui exécute les tâches envoyées de manière séquentielle (comme le thread de répartition d'événements Swing). |
newSheduledThreadPool() | Un pool de threads fixe pour une exécution programmée, remplace java.util.Timer. |
newSingleThreadScheduledExecutor() | Un "pool" d'un seul thread pour une exécution programmée. |
Evaluons les premières méthodes, nous verrons les autres par la suite :
Ces trois méthodes renvoient un objet de la classe ThreadPoolExecutor qui implémente l'interface ExecutorService.
§
Vous pouvez envoyer un Runnable ou un Callable à un ExecutorService à l'aide de l'une de ses méthodes submit() :
Future<?> submit(Runnable tâche) Future<T> submit(Runnable tâche, T résultat) Future<T> submit(Callable<T> tâche)
Le pool exécutera la tâche envoyée le plus tôt possible. Lorsque vous appelez la méthode submit(), vous recevez un objet Future disponible pour enquêter sur l'état de la tâche.
Lorsque vous avez terminé avec un pool de connexion, appelez la méthode shutdown(). Cette méthode lance la séquence de fermeture du pool. Un exécuteur fermé n'accepte plus de nouvelles tâches. Lorsque toutes les tâches sont terminées, les threads du pool meurent. Vous pouvez aussi appeler la méthode shutdownNow(). Le pool annule alors toutes les tâches qui n'ont pas encore commencé et tente d'interrompre les threads en cours d'exécution.
Voici, en bref, ce qu'il faut faire pour utiliser un pool de connexion :
L'exemple précédent produisait un grand nombre de threads courts, un par répertoire. Nous allons reprendre cet exemple en prenant en compte cette fois-ci un pool de threads pour lancer ces tâches. Vous remarquerez que la réactivité s'en trouve largement améliorée.
run:
Mot à rechercher : tâche
6 fichiers trouvés
Taille du pool = 2147483647
BUILD SUCCESSFUL (total time: 25 seconds)
package synchronisation; import java.io.*; import java.util.*; import java.util.concurrent.*; public class RetourValeur { private static String répertoire = ; public static void main(String[] args) { Scanner clavier = new Scanner(System.in); System.out.print( ); String mot = clavier.nextLine(); ExecutorService pool = Executors.newCachedThreadPool(); TâcheDeRecherche rechercher = new TâcheDeRecherche(new File(répertoire), mot, pool); Future<Integer> résultat = pool.submit(rechercher); try { System.out.println(résultat.get() + ); } catch (ExecutionException e) { e.printStackTrace(); } catch (InterruptedException e) { } pool.shutdown(); int taille = ((ThreadPoolExecutor)pool).getMaximumPoolSize(); System.out.println( +taille); } } class TâcheDeRecherche implements Callable<Integer> { private File répertoire; private String mot; private int compteur; private ExecutorService pool; public TâcheDeRecherche(File répertoire, String mot, ExecutorService pool) { this.répertoire = répertoire; this.mot = mot; this.pool = pool; } public Integer call() { compteur = 0; try { File[] fichiers = répertoire.listFiles(); ArrayList<Future<Integer>> résultats = new ArrayList<Future<Integer>>(); for (File fichier : fichiers) if (fichier.isDirectory()) { TâcheDeRecherche rechercher = new TâcheDeRecherche(fichier, mot, pool); Future<Integer> résultat = pool.submit(rechercher); résultats.add(résultat); } else if (recherche(fichier)) compteur++; for (Future<Integer> résultat : résultats) try { compteur += résultat.get(); } catch (ExecutionException e) { e.printStackTrace(); } } catch (InterruptedException e) { } return compteur; } public boolean recherche(File fichier) { try { Scanner lecture = new Scanner(new FileInputStream(fichier)); boolean trouvé = false; while (!trouvé && lecture.hasNextLine()) { String ligne = lecture.nextLine(); if (ligne.contains(mot)) trouvé = true; } lecture.close(); return trouvé; } catch (IOException e) { return false; } } }
Vous avez vu comment utiliser un service exécuteur sous forme de pool de threads pour augmenter l'efficacité de l'exécution de la tâche. Un exécuteur sert parfois à des fins plus tactiques, simplement pour contrôler un groupe de tâches liées.
Ainsi, supposons que vous deviez factoriser un grand entier (un calcul requis pour casser un code RSA). Vous pourriez envoyer plusieurs tâches, chacune tentant une factorisation à l'aide de nombres d'une plage différente. Dès que l'une de ces tâches a une réponse, votre calcul peut s'arrêter.
List<Callable<T>> tâches = ... ; List<Future<T>> résultats = exécuteur.invokeAll(tâches); for (Future<T> résultat : résultats) traitement(résultat.get());
L'inconvénient de cette méthode, c'est que vous risquez d'attendre sans raison valable si la première tâche prend du temps. Il serait plus logique d'obtenir les résultats dans l'ordre de leur disponibilité. Cela peut être arrangé avec ExecutorCompletionService.
Commencer par un exécuteur, obtenu de la manière ordinaire. Construisez ensuite un ExecutorCompletionService. Envoyer les tâches au service de réalisation. Le service gère une queue de blocage d'objets Future, contenant les résultats des tâches soumises lorqu'elles deviennent disponibles.
Une organisation plus efficace pour le calcul précédent correspond à ceci :List<Callable<T>> tâches = ... ;
ExecutorCompletionService service = new ExecutorCompletionService(exécuteur); for (Callable<T> tâche : tâches) service.submit(tâche);
for (int i=0; i<tâches.size(); i++) traitement(service.take().get());
Lorsque nous devons gérer beaucoup de petites tâches élémentaires, nous l'avons dit, il est beaucoup plus avantageux en termes de ressource et de performance, de prendre de pool de threads que nous avons nommé exécuteur. L'exemple, par excellence qui si prête bien, est notre petite application sur les balles rebondissantes.
La création du pool de thread se fait lorsque nous cliquons sur le bouton "Démarrer". A chaque fois que nous cliquons sur ce bouton, une nouvelle balle est créée dans son propre thread, et elle est rajoutée dans l'exécuteur. Si par contre vous cliquez sur le bouton "Arrêter", le pool de thread s'arrête définitivement, et du même coup, interrompt tous les threads. Lorsque de nouveau, vous cliquez sur le bouton "Démarrer", un nouvel exécuteur est créé.
package threads; import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.util.ArrayList; import java.util.concurrent.*; import javax.swing.*; public class PlusieursBalles extends JFrame { private JButton démarrer = new JButton( ); private JButton arrêter = new JButton( ); private JPanel boutons = new JPanel(); private Panneau panneau = new Panneau(); public PlusieursBalles() { super( ); panneau.setBackground(Color.ORANGE); add(panneau); add(boutons, BorderLayout.SOUTH); boutons.add(démarrer); boutons.add(arrêter); démarrer.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { panneau.ajoutBalle(); } }); arrêter.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { panneau.arrêtBalles(); } }); setSize(500, 400); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } private class Panneau extends JPanel { private ExecutorService exécuteur; private ArrayList<Balle> balles = new ArrayList<Balle>(); public void ajoutBalle() { if (exécuteur == null) exécuteur = Executors.newCachedThreadPool(); Balle balle = new Balle(); balles.add(balle); exécuteur.submit(balle); } public void arrêtBalles() { if (exécuteur != null) { exécuteur.shutdownNow(); exécuteur = null; balles.clear(); } } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D surface = (Graphics2D) g; for (Balle balle : balles) surface.fill(balle.getForme()); } } private class Balle implements Runnable { private double x, y, dx=5, dy=5; public void déplace(Rectangle2D zone) { x+=dx; y+=dy; if (x < zone.getMinX()) { x = zone.getMinX(); dx = -dx; } if (x+15 >= zone.getMaxX()) { x = zone.getMaxX() - 15; dx = -dx; } if (y < zone.getMinY()) { y = zone.getMinY(); dy = -dy; } if (y+15 >= zone.getMaxY()) { y = zone.getMaxY() - 15; dy = -dy; } } public Ellipse2D getForme() { return new Ellipse2D.Double(x, y, 15, 15); } public void run() { try { while (true) { déplace(panneau.getBounds()); panneau.repaint(); Thread.sleep(5); } } catch (InterruptedException ex) { } } } public static void main(String[] args) { new PlusieursBalles(); } }
Le paquetage java.util.concurrent contient plusieurs classes qui permettent de gérer un jeu de threads de collaboration. Ces mécanismes possèdent des fonctionnalités intégrées pour les motifs communs de rencontre des threads.
Si vous disposez d'un jeu de threads de collaboration qui suit l'un de ces motifs de comportement, réutilisez simplement la classe de bibliothèque appropriée au lieu de concocter une collection artisanale de verrous et de conditions.
Classe | Action | Quand l'utiliser |
---|---|---|
CyclicBarrier | Permet à un jeu de threads de patienter jusqu'à ce qu'un nombre prédéfini d'entre eux ait atteint une barrière commune, puis exécute, en option, une action de barrière. | Lorsque plusieurs threads doivent se terminer avant que leurs résultats ne puissent être utilisées. |
CountDownLatch | Permet à un jeu de threads de patienter jusqu'à ce qu'un compteur ait été ramené à 0. | Lorsqu'un ou plusieurs threads doivent patienter jusqu'à ce qu'un nombre spécifié de résultats soit disponibles. |
Exchanger | Permet à deux threads d'échanger des objets lorsque les deux sont prêts pour l'échange. | Lorsque deux threads agissent sur deux instances de la même structure de données, l'un en remplissant une instance, l'autre en vidant l'autre instance. |
SynchronousQueue | Permet à un thread de donner un objet à un autre thread. | Pour envoyer un objet d'un thread à un autre lorsque les deux sont prêts, sans synchronisation explicite. |
Semaphore | Permet à un jeu de threads de patienter jusqu'à ce que les autorisations de poursuivre soient disponibles. | Pour limiter le nombre total de threads ayant accès à une ressource. Si le compte des autorisations est à un, à utiliser pour bloquer les threads jusqu'à ce qu'un autre thread donne une autorisation. |
Un sémaphore est un élément qui gère plusieurs autorisations.
Mais, il ne s'agit pas de vrais objets d'autorisation. Le sémaphore conserve simplement un compteur. De plus, une autorisation n'a pas à être libérée par le thread qui l'acquiert. En fait, n'importe quel thread peut émettre n'importe quel nombre d'autorisation. S'il en émet plus que le maximum disponible, le sémaphore est simplement défini sur le compte maximal. Ils sont donc flexibles, mais peuvent paraître confus.
Les sémaphores ont été inventés par Edsger Dijkstra en 1968 pour être utilisés sous la forme de primitives de synchronisation. Mr Dijkstra a montré que les sémaphores pouvaient être efficacement implémentés et qu'ils sont suffisamment performants pour résoudre les problèmes communs de synchronisations des threads. Dans presque tous les systèmes d'exploitation, vous trouverez des implémentations de queues bornées utilisant des sémaphores.
Bien entendu, les programmeurs d'application n'ont pas à réinventer les queues bornées. Nous vous suggérons de n'utiliser les sémaphores que lorsque leur comportement concorde bien avec votre problème de synchronisations.
Par exemple, un sémaphore ayant un nombre d'autorisations égal à 1 servira de porte qu'un autre thread pourra ouvrir ou fermer. Vous verrez un peu plus loin un exemple dans lequel un thread travailleur produit une animation. Parfois, le thread travailleur attend qu'un utilisateur appuie sur un bouton. Le thread travailleur tente d'acquérir une autorisation, puis attend qu'un clic de bouton autorise l'émission d'une autorisation.
Un CountDownLatch permet à un jeu de threads de patienter jusqu'à ce qu'un compteur ait atteint zéro. Le CountDownLatch ne sert qu'une fois. Lorsqu'un compteur a atteint zéro, vous ne pouvez pas le réutiliser.
Il existe un cas spécial, où le compteur du verrou est à 1. Ceci implémente une porte d'utilisation unique. Les threads sont maintenus à la porte jusqu'à ce qu'un autre thread définissent le compte à 0.
Imaginons, par exemple, un jeu de threads qui ait besoin de données initiales pour effectuer son travail. Les threads travailleurs démarrent, puis patientent à la porte. Un autre thread prépare les données. Lorsqu'il est prêt, il appelle la méthode countDown() et tous les threads travailleurs poursuivent.
Vous pouvez utiliser un second verrou pour vérifier lorsque tous les threads travailleurs sont effectués. Initialisez le verrou avec le nombre de threads. Chaque thread travaileur décompte ce verrou juste avant de terminer. Un autre thread qui récolte les résultats du travail patiente sur le verrou et continue dès que tous les travailleurs ont terminés.
La classe CyclicBarrier implémente ce que l'on appelle une barrière. Imaginons que plusieurs threads travaillent sur certaines parties d'un calcul. Lorsque toutes les parties sont prêtes, il faut combiner les résultats. Ainsi, lorsqu'un thread a terminé avec une partie, nous le laissons s'exécuter contre la barrière. Lorsque tous les threads ont atteint la barrière, celle-ci cède et les threads peuvent poursuivre.
CyclicBarrier barrière = new CyclicBarrier(nombreDeThreads);
public void run() { faireTravail(); barrière.await(); }
barrière.await(100, TimeUnit.MILLISECONDS);
Runnable actionBarrière = ...;
CyclicBarrier barrière = new CyclicBarrier(nombreDeThreads, actionBarrière);
La barrière est dite cyclique car elle peut être réutilisée après libération de tous les threads en attente. Elle diffère en cela d'un CountDownLatch, qui ne peut être utilisé qu'une seule fois.
Exchanger est utilisé lorsque deux threads travaillent sur deux instances du même tampon de données. Généralement, un thread remplit le tampon, tandis que l'autre consomme son contenu. Lorsque les deux ont terminé, ils échangent leurs tampons.
Une queue synchrone est un mécanisme qui apparie un thread producteur et un thread consommateur. Lorsqu'un thread appelle la méthode put() sur un SynchronousQueue, il bloque jusqu'à ce qu'un autre thread appelle la méthode take(), et vice versa. Â la différence d'Exchanger, les données ne sont transférées que dans une direction, du producteur au consommateur.
Nous allons reprendre notre application sur les balles rebondissantes. Cette fois-ci, nous contrôlons parfaitement l'animation des balles déjà présentes. Il est possible maintenant de faire une pause et ainsi de figer l'ensemble des balles et de les relancer par la suite lorsque nous le désirons.
Un sémaphore avec un compteur d'autorisation de 1 peut servir à synchroniser l'ensemble des threads représentant le mouvement des balles. Si nous demandons de mettre en pause avec le bouton "Pause", chaque thread de l'animation appelle la méthode acquire(). Quand plus tard l'utilisateur clique sur le bouton "Relancer", le thread d'interface utilisateur appelle la méthode release(), en spécifiant le nombre de threads à libérer (correspondant au nombre de balles déjà présentes). Ainsi toutes les balles redeviennent animées.
package threads; import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.util.ArrayList; import java.util.concurrent.*; import javax.swing.*; public class PlusieursBalles extends JFrame { private JToolBar barre = new JToolBar(); private Panneau panneau = new Panneau(); private Semaphore barrière = new Semaphore(1); private boolean animation = false; public PlusieursBalles() { super( ); panneau.setBackground(Color.GREEN); add(panneau); add(barre, BorderLayout.SOUTH); barre.setLayout(new FlowLayout()); barre.add(new AbstractAction( ) { public void actionPerformed(ActionEvent e) { panneau.ajoutBalle(); } }); barre.add(new AbstractAction( ) { public void actionPerformed(ActionEvent e) { panneau.pause(); } }); barre.add(new AbstractAction( ) { public void actionPerformed(ActionEvent e) { panneau.relancer(); } }); barre.add(new AbstractAction( ) { public void actionPerformed(ActionEvent e) { panneau.toutRecommencer(); } }); setSize(500, 400); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } private class Panneau extends JPanel { private ExecutorService exécuteur; private ArrayList<Balle> balles = new ArrayList<Balle>(); public void ajoutBalle() { if (exécuteur == null) exécuteur = Executors.newCachedThreadPool(); Balle balle = new Balle(); balles.add(balle); exécuteur.submit(balle); if (!animation) relancer(); } public void pause() { animation = false; } public void relancer() { animation = true; barrière.release(balles.size()); } public void toutRecommencer() { if (exécuteur != null) { exécuteur.shutdownNow(); exécuteur = null; balles.clear(); ajoutBalle(); } } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D surface = (Graphics2D) g; for (Balle balle : balles) surface.fill(balle.getForme()); } } private class Balle implements Runnable { private double x, y, dx=5, dy=5; public void déplace(Rectangle2D zone) { x+=dx; y+=dy; if (x < zone.getMinX()) { x = zone.getMinX(); dx = -dx; } if (x+15 >= zone.getMaxX()) { x = zone.getMaxX() - 15; dx = -dx; } if (y < zone.getMinY()) { y = zone.getMinY(); dy = -dy; } if (y+15 >= zone.getMaxY()) { y = zone.getMaxY() - 15; dy = -dy; } } public Ellipse2D getForme() { return new Ellipse2D.Double(x, y, 15, 15); } public void run() { try { while (true) { if (animation) { déplace(panneau.getBounds()); panneau.repaint(); Thread.sleep(5); } else barrière.acquire(); } } catch (InterruptedException ex) { } } } public static void main(String[] args) { new PlusieursBalles(); } }
Nous l'avons vu dans l'introduction de cette étude, l'une des raisons poussant à utiliser les threads dans vos programmes est que ces derniers répondront mieux. Losqu'un programme doit réaliser une tâche de longue durée, vous pouvez lancer un autre travailleur au lieu de bloquer l'interface utilisateur.
Attention toutefois à ce que vous faites dans un thread travailleur car, et cela peut surprendre, Swing n'est pas compatible avec les threads. Si vous tentez de manipuler des éléments graphiques à partir de plusieurs threads, l'interface utilisateur peut s'endommager.
Pour constater ce problème, exécutez le programme suivant. Lorsque vous cliquez sur le bouton "Mauvais", un nouveau thread est démarré. Sa méthode run() torture une liste déroulante, en ajoutant et supprimant des valeurs de manière aléatoire
import java.awt.FlowLayout; import java.awt.event.*; import java.util.*; import javax.swing.*; class ThreadSwing extends JFrame { private JComboBox combo = new JComboBox(); private JButton mauvaisBouton = new JButton( ); public ThreadSwing() { setTitle( ); combo.insertItemAt(Integer.MAX_VALUE, 0); combo.setPrototypeDisplayValue(combo.getItemAt(0)); combo.setSelectedIndex(0); mauvaisBouton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { new Thread(new MauvaisThread(combo)).start(); } }); add(mauvaisBouton); add(combo); setLayout(new FlowLayout()); pack(); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public static void main(String[] a) { new ThreadSwing(); } } class MauvaisThread implements Runnable { private JComboBox combo; private Random générateur; public MauvaisThread(JComboBox aCombo) { combo = aCombo; générateur = new Random(); } public void run() { try { while (true) { int i = Math.abs(générateur.nextInt()); if (i % 2 == 0) combo.insertItemAt(i, 0); else if (combo.getItemCount() > 0) combo.removeItemAt(i % combo.getItemCount()); Thread.sleep(1); } } catch (InterruptedException e) { } } }
Testez-le. Cliquez sur le bouton "Mauvais", puis cliquez plusieurs fois sur la zone déroulante. Déplacez la barre de défilement. Déplacez la fenêtre. Cliquez à nouveau sur le bouton "Mauvais". Continuez à cliquer sur la zone déroulante. Enfin, vous devez avoir un rapport d'exception.
Que se passe-t-il ? Lorsqu'un élément est inséré dans la zone déroulante, celle-ci déclenche un événement pour actualiser l'affichage. Le code d'affichage se met ensuite en route, il lit la taille actuelle de la zone déroulante, puis se prépare à afficher les valeurs. Mais le thread travailleur continue à fonctionner, entraînant parfois une réduction du nombre des valeurs dans la zone déroulante. Le code d'affichage pense alors qu'il y a plus de valeurs dans le modèle que ce qu'il y en a réellement. Il demande des valeurs inexistantes, puis déclenche une exception ArrayIndexOutOfBounds.
Lorsque vous utilisez des threads avec Swing, deux règles simples doivent être respectées :
La raison de cette première règle est simple à comprendre. Si vous passez trop de temps dans le thread de répartition d'événements, l'application semble
car elle ne répond plus à aucun événement.Notamment, le thread de répartition d'événements ne doit jamais réaliser d'appels en entrée/sortie, qui pourraient se bloquer indéfiniment, et il ne doit jamais appeler la méthode sleep() (si vous devez attendre un délai spécifique, utilisez les événements de minuteur).
La seconde règle est souvent appeléeSwing. Nous la découvrirons un peu plus loin.
§
Par exemple, pour actualiser une barre de progression ou une étiquette de texte, vous ne pouvez pas simplement régler sa valeur à partir de votre thread.
Pour résoudre ce problème, il existe deux méthodes dans tous les threads pour ajouter des actions arbitraires à la queue des événements. Ainsi, supposons que vous vouliez régulièrement actualiser une étiquette dans un thread pour signaler une progression. Vous ne pouvez pas appeler la méthode setText() de l'étiquette à partir du thread. Utilisez plutôt les méthodes invokeLater() et invokeAndAwait() de la classe EventQueue pour que cet appel soit exécuté dans le thread de répartition des événements.
EventQueue.invokeLater(new Runnable() { public void run() { étiquette.setText("Progression : "+progression); } });
Dans le cas d'une mise à jour d'une étiquette de progression, la méthode invokeLater() convient mieux. Les utilisateurs préfère que le thread travailleur continue à évoluer, plutôt que d'avoir un indicateur de progression plus précis.
Les deux méthodes exécutent la méthode run() dans le thread de répartition des événements. Aucun nouveau thread n'est créé.
§
Reprenons l'exemple précédent. Nous rajoutons un bouton dénommé "Bon". Ce nouveau code nous montre comment utiliser la méthode invokeLater() pour modifier en toute sécurité le contenu d'une zone déroulante.
Si vous cliquez sur le bouton "Bon", un thread insère et supprime des nombres. Toutefois, la modification réelle a lieu dans le thread de répartition des événements.
import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; class ThreadSwing extends JFrame { private JComboBox combo = new JComboBox(); private JToolBar boutons = new JToolBar(); public ThreadSwing() { setTitle( ); combo.insertItemAt(Integer.MAX_VALUE, 0); combo.setPrototypeDisplayValue(combo.getItemAt(0)); combo.setSelectedIndex(0); boutons.add(new AbstractAction( ) { public void actionPerformed(ActionEvent e) { new Thread(new MauvaisThread(combo)).start(); } }); boutons.add(new AbstractAction( ) { public void actionPerformed(ActionEvent e) { new Thread(new BonThread(combo)).start(); } }); boutons.add(combo); add(boutons); setLayout(new FlowLayout()); pack(); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public static void main(String[] a) { new ThreadSwing(); } } class MauvaisThread implements Runnable { private JComboBox combo; private Random générateur; public MauvaisThread(JComboBox unCombo) { combo = unCombo; générateur = new Random(); } public void run() { try { while (true) { int i = Math.abs(générateur.nextInt()); if (i % 2 == 0) combo.insertItemAt(i, 0); else if (combo.getItemCount() > 0) combo.removeItemAt(i % combo.getItemCount()); Thread.sleep(1); } } catch (InterruptedException e) { } } } class BonThread implements Runnable { private JComboBox combo; private Random générateur; public BonThread(JComboBox unCombo) { combo = unCombo; générateur = new Random(); } public void run() { while (true) { try { EventQueue.invokeLater(new Runnable() { public void run() { int i = Math.abs(générateur.nextInt()); if (i % 2 == 0) combo.insertItemAt(i, 0); else if (combo.getItemCount() > 0) combo.removeItemAt(i % combo.getItemCount()); } }); Thread.sleep(1); } catch (InterruptedException ex) { } } } }
Lorsqu'un utilisateur émet une commande pour laquelle le traitement est long, vous devez déclencher un nouveau thread pour qu'il effectue le travail. Nous venons de le voir, ce thread doit utiliser la méthode invokeLater() de la classe EventQueue pour actualiser l'interface utilisateur.
Il existe maintenant, à partir de Java SE 6.0, une classe vraiment intéressante qui simplifie encore plus le travail, et qui permet surtout de suivre une progression. Il s'agit de la classe SwingWorker.
Afin de bien maîtriser cette classe SwingWorker, nous allons mettre en oeuvre une petite application qui possède des commandes pour charger un fichier texte et annuler le traitement de chargement. Tester le programme avec un fichier relativement long.
private class ProgressionDonnées { public int pourcentage; public String ligne; }
@Override public StringBuilder doInBackground() throws IOException, InterruptedException { int nombreOctets = 0; Scanner entrée = new Scanner(new FileInputStream(fichier)); while (entrée.hasNextLine()) { String ligne = entrée.nextLine(); nombreOctets += ligne.length()+2; texte.append(ligne); texte.append(\n ); ProgressionDonnées données = new ProgressionDonnées(); données.pourcentage = (int) (nombreOctets * 100 / taille); données.ligne = ligne; publish(données); Thread.sleep(1); // pour tester l'annulation ; inutile dans vos programmes } return texte; }
Nous appelons également la méthode sleep() pendant un millième de seconde après chaque ligne pour que vous puissiez tester l'annulation sans être stressé, mais vous ne voulez pas ralentir vos programmes avec sleep(). Si vous commentez cette ligne, vous verrez que votre texte se charge très rapidement, avec seulement quelques mise à jour d'interface utilisateur par lot.
Vous pouvez amener ce programme à se comporter de manière assez homogène en actualisant la zone de texte à partir du thread travailleur, mais ce n'est pas possible pour la plupart des composants Swing. Nous montrons ici l'approche générale dans laquelle les mises à jour des composants surviennent dans le thread de répartition des événements.
@Override public void process(List<ProgressionDonnées> données) { if (isCancelled()) return; StringBuilder nouvelleLigne = new StringBuilder(); int pourcentage = données.get(données.size() - 1).pourcentage; progression.setValue(pourcentage); commentaire.setText( +pourcentage+ ); for (ProgressionDonnées donnée : données) { nouvelleLigne.append(donnée.ligne); nouvelleLigne.append(\n ); } zoneDeTexte.append(nouvelleLigne.toString()); }
@Override public void done() { try { StringBuilder résultat = get(); zoneDeTexte.setText(résultat.toString()); commentaire.setText( ); } catch (InterruptedException ex) { } catch (CancellationException ex) { zoneDeTexte.setText( ); commentaire.setText( ); } catch (ExecutionException ex) { commentaire.setText( + ex.getCause()); } annuler.setEnabled(false); ouvrir.setEnabled(true); }
import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; import java.util.List; import java.util.concurrent.*; import javax.swing.*; public class ThreadSwing extends JFrame { private JFileChooser sélection = new JFileChooser();; private JTextArea zoneDeTexte = new JTextArea(); private JToolBar boutons = new JToolBar(); private Action ouvrir; private Action annuler; private JProgressBar progression = new JProgressBar(); private JLabel commentaire = new JLabel( ); private SwingWorker<StringBuilder, ProgressionDonnées> texte; public ThreadSwing() { sélection.setCurrentDirectory(new File( )); add(new JScrollPane(zoneDeTexte)); setSize(450, 350); add(progression, BorderLayout.SOUTH); boutons.add(ouvrir = new AbstractAction( ) { public void actionPerformed(ActionEvent event) { int résultat = sélection.showOpenDialog(null); if (résultat == JFileChooser.APPROVE_OPTION) { zoneDeTexte.setText( ); ouvrir.setEnabled(false); texte = new LectureTexte(sélection.getSelectedFile()); texte.execute(); annuler.setEnabled(true); } } }); boutons.add(annuler = new AbstractAction( ) { public void actionPerformed(ActionEvent event) { texte.cancel(true); } }); boutons.addSeparator(); boutons.add(commentaire); add(boutons, BorderLayout.NORTH); annuler.setEnabled(false); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public static void main(String[] args) { new ThreadSwing(); } private class ProgressionDonnées { public int pourcentage; public String ligne; } private class LectureTexte extends SwingWorker<StringBuilder, ProgressionDonnées> { private File fichier; private StringBuilder texte = new StringBuilder(); private long taille; public LectureTexte(File fichier) { this.fichier = fichier; taille = fichier.length(); } @Override public StringBuilder doInBackground() throws IOException, InterruptedException { int nombreOctets = 0; Scanner entrée = new Scanner(new FileInputStream(fichier)); while (entrée.hasNextLine()) { String ligne = entrée.nextLine(); nombreOctets += ligne.length()+2; texte.append(ligne); texte.append(\n ); ProgressionDonnées données = new ProgressionDonnées(); données.pourcentage = (int) (nombreOctets * 100 / taille); données.ligne = ligne; publish(données); Thread.sleep(1); // pour tester l'annulation ; inutile dans vos programmes } return texte; } @Override public void process(List<ProgressionDonnées> données) { if (isCancelled()) return; StringBuilder nouvelleLigne = new StringBuilder(); int pourcentage = données.get(données.size() - 1).pourcentage; progression.setValue(pourcentage); commentaire.setText( +pourcentage+ ); for (ProgressionDonnées donnée : données) { nouvelleLigne.append(donnée.ligne); nouvelleLigne.append(\n ); } zoneDeTexte.append(nouvelleLigne.toString()); } @Override public void done() { try { StringBuilder résultat = get(); zoneDeTexte.setText(résultat.toString()); commentaire.setText( ); } catch (InterruptedException ex) { } catch (CancellationException ex) { zoneDeTexte.setText( ); commentaire.setText( ); } catch (ExecutionException ex) { commentaire.setText( + ex.getCause()); } annuler.setEnabled(false); ouvrir.setEnabled(true); } } }
Chaque application Java démarre par une méthode main() qui s'exécute dans le thread principal. Dans un programme Swing, le thread principal est court. Il programme la construction de l'interface utilisateur dans le thread de répartition des événements puis s'arrête. Une fois l'interface utilisateur construite, le thread de répartition des événements traite les notifications des événements, comme les appels à actionPerformed() ou paintComponent(). D'autres threads, comme celui qui envoie les événements dans le queue des événements, s'exécutent en coulisse mais ils sont invisibles pour le programmeur.
Précédemment, au cours de ce chapitre, nous avons présenté la règle du thread unique : "Ne touchez pas aux composants Swing d'un thread autre que le thread de répartition des événements". Nous allons détailler cette règle.
Il existe quelques exceptions :
Historiquement, la règle du thread unique était plus permissive. N'importe quel thread avait le droit de construire des composants, de définir leurs propriétés et de les ajouter dans des conteneurs, tant qu'aucun des composants n'avait été réalisé. Un composant est réalisé s'il peut recevoir des événements de dessin ou de validation. C'est la cas dès que les méthodes setVisible(true) ou pack() ont été appelées sur le composant ou si le composant a été ajouté à un conteneur qui a été réalisé.
Cette version de la règle du thread unique était commode. Elle nous permettait de créer l'interface graphique dans la méthode main(), puis d'appeler setVisible(true) sur le cadre de niveau supérieur de l'application. Il n'existait aucun planification gênante d'un Runnable sur le thread de répartition des événements.
Malheureusement, certains implémenteurs de composants ne font pas attention aux subtilités de la règle du thread unique. Ils lancent des activités sur le thread de répartition des événements sans jamais s'inquiéter de vérifier si le composant est réalisé. Ainsi, si vous appelez setSelectionStart() ou setSelectionEnd() sur un JTextComponent, un mouvement du curseur de texte est planifié dans le thread de répartition des événements, même si le composant n'est pas visible.
Il aurait pu être possible de détecter et de résoudre ces problèmes, mais les concepteurs de Swing ont choisi la solution de facilité. Ils ont décrété qu'il n'était jamais sûr d'accéder aux composants depuis n'importe quel thread de répartition des événements.
class ThreadSwing extends JFrame { ... public ThreadSwing() { ... } public static void main(String[] a) throws Exception { EventQueue.invokeLater(new Runnable() { public void run() { new ThreadSwing(); } }); } }
Il existe bien entendu de nombreux programmes qui ne sont pas attentifs et concervent l'ancienne version de la règle du thread unique, en initialisant l'interface utilisateur sur le thread principal. Ces programmes portent le léger risque qu'une partie de l'initialisation de l'interface utilisateur entraîne des actions sur le thread de répartition des événements entrant en conflit avec les actions du thread principal.
Afin de bien maîtriser le concept de thread avec les composants Swing, je vous propose de mettre en oeuvre un système client-serveur qui permet de transférer des gros fichiers sur le réseau et de contrôler ainsi la progression du transfert.
Deux programmes doivent donc être mise en place, d'une part le service qui stocke (uniquement, c'est un cas d'école) le fichier désiré dans un répertoire prédéfini, et d'autre part le client qui permet de choisir le fichier et de l'envoyer ensuite au service, sur le poste distant. Voici, la représentation de la partie cliente.
Vous pourrez rajouter par la suite des commandes supplémentaires, à la fois sur le client et le serveur, afin de pouvoir récupérer les fichiers stockés, de supprimer les fichiers déjà enregistrés, de lister les fichiers déjà présents, etc.
package réseau; import java.awt.*; import java.awt.event.ActionEvent; import java.io.*; import java.net.Socket; import java.util.List; import java.util.concurrent.ExecutionException; import javax.swing.*; public class Client extends JFrame { private JFileChooser sélection = new JFileChooser(); private JToolBar boutons = new JToolBar(); private JTextField résultat = new JTextField( ); private JProgressBar progression = new JProgressBar(); private final int BUFFER = 4096; public Client() { super( ); add(boutons, BorderLayout.NORTH); boutons.add(new AbstractAction( ) { public void actionPerformed(ActionEvent e) { if (sélection.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) new TransfertFichier(sélection.getSelectedFile()).execute(); } }); boutons.addSeparator(); boutons.add(progression); boutons.addSeparator(); progression.setStringPainted(true); progression.setVisible(false); résultat.setEditable(false); résultat.setMargin(new Insets(3, 3, 3, 3)); add(résultat, BorderLayout.SOUTH); pack(); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public static void main(String[] args) { new Client(); } private class TransfertFichier extends SwingWorker<String, Integer> { private File fichier; private int octetsLus; public TransfertFichier(File fichier) { this.fichier = fichier; } @Override protected String doInBackground() throws Exception { try { résultat.setText( +fichier.getName()+ ); progression.setVisible(true); progression.setValue(0); BufferedInputStream lecture = new BufferedInputStream(new FileInputStream(fichier)); progression.setMaximum(lecture.available()); Socket service = new Socket( , 5588); ObjectOutputStream envoi = new ObjectOutputStream(new BufferedOutputStream(service.getOutputStream())); envoi.writeObject(fichier.getName()); envoi.writeInt(lecture.available()); envoi.writeInt(BUFFER); byte[] octets = new byte[BUFFER]; while (lecture.available() > 0) { if (lecture.available() < BUFFER) octets = new byte[lecture.available()]; lecture.read(octets); envoi.write(octets); envoi.flush(); publish(octetsLus += BUFFER); } lecture.close(); service.close(); return +fichier.getName()+ ; } catch (FileNotFoundException ex) { return ; } catch (IOException ex) { return ; } } @Override protected void process(List<Integer> nombres) { progression.setValue(nombres.get(nombres.size()-1)); } @Override protected void done() { try { résultat.setText(get()); progression.setVisible(false); } catch (Exception ex) { } } } }
package réseau; import java.io.*; import java.net.*; public class Serveur { private static String stockage = ; public static void main(String[] args) throws IOException, ClassNotFoundException { ServerSocket service = new ServerSocket(5588); while (true) { Socket client = service.accept(); try { ObjectInputStream réception = new ObjectInputStream(new BufferedInputStream(client.getInputStream())); String nom = (String) réception.readObject(); int taille = réception.readInt(); int buffer = réception.readInt(); int nombre = taille / buffer; byte[] octets = new byte[buffer]; byte[] reste = new byte[taille % buffer]; BufferedOutputStream fichier = new BufferedOutputStream(new FileOutputStream(stockage + nom)); for (int i = 0; i < nombre; i++) { réception.readFully(octets); fichier.write(octets); } réception.readFully(reste); fichier.write(reste); fichier.close(); client.close(); } catch (Exception ex) {} } } }
Nous avons vu combien il était facile de créer et de manipuler des threads multiples s'exécutant au sein d'un même interpréteur Java. Java possède également une classe java.lang.Process qui représente un autre programme s'exécutant de manière externe à l'interpréteur. Un programme Java peut communiquer ensuite avec un processus externe en utilisant des flux de la même manière qu'il peut communiquer avec un serveur sur un autre ordinateur du réseau. L'utilisation d'un objet Process est toujours dépendant de la plate-forme et rarement portable, mais s'avère parfois utile.
Pour communiquer entre ordinateurs, voir
la leçon sur les Sockets.
§
En fait, pour exécuter un autre programme (ou autre processus) depuis notre programme actuel, il est nécessaire d'utiliser deux classes. La première est Process comme nous venons de le voir et qui est utile pour lancer un nouveau processus à partir du processus actuel. La seconde est java.lang.Runtime qui permet de connaitre toutes les informations relatives à l'environnement de la machine comme par exemple le système d'exploitation.
Cette classe décrit un processus à exécuter dans un environnement externe à l'interpréteur Java. Notez qu'un objet Process est tout à fait différent d'un objet Thread ; la classe Process est abstraite et ne peut pas être instanciée. Appelez alors l'une des méthodes Runtime.exec() pour démarrer un processus et retourner l'objet Process correspondant.
Cette classe encapsule un certain nombre de fonctions système dépendant de la plate-forme. C'est cette classe qui permet de connaitre notamment le système d'exploitation utilisé ainsi que ses commandes annexes.
Après toutes ces considérations, nous allons demander dans notre processus principal de récupérer toutes les informations concernant le réseau grâce à la commande ipconfig du système d'exploitation. Toutes ces informations seront ensuite récupérées dans un flot de texte d'entrée et ensuite envoyées dans le flot de sortie principal, c'est à dire sur l'écran.
Il est bien évident que ce programme ne sert absolument à rien puisque il suffit de tapez directement cette commande depuis le système d'exploitation pour obtenir exactement le même résultat. Toutefois, ce petit programme simple nous permet de comprendre tous les mécanismes en jeu.
Code du processus principal
Résultat obtenu
Dans un certain nombre d'environnement de programmation, vous pouvez définir des chronomètres. Un chronomètre sert à prevenir les éléments de votre programme à intervalles réguliers. Par exemple, pour afficher une horloge dans une fenêtre, l'objet horloge doit être averti une fois par seconde.
A titre d'exemple, le programme suivant indique le temps écoulé en secondes depuis le lancement de l'application.
Bien qu'il y ait très peu de chose à écrire, il est possible de faire appel à l'expert "Implémenter interface..." pour construire automatiquement toute l'ossature, à la fois pour ActionListener, et également pour actionPerformed.