Threads et performance avec SwingDate de publication : 19/06/2006
Le toolkit Swing permet aux développeurs Java de réaliser des applications graphiques très complexes.
Sa complexité rend malheureusement aisé la réalisation d'interfaces présentant de piètres performances.
Introduction 1. Le modèle de gestion des threads 2. Gestion avancée des tâches Introduction
Swing repose entièrement sur son prédécesseur, l'Abstract Windowing Toolkit (AWT).
Ces deux technologies diffèrent non seulement par leurs API et leurs fonctionnalités,
mais surtout par leur nature. Les composants AWT sont en effet liés à des composants natifs
dont ils se servent pour l'affichage.
C'est pourquoi AWT est dit heavyweight, ou lourd. Swing, au contraire, prend complètement
en charge la gestion des composants, qui sont dessinés par du code en pur Java.
Swing est donc dit lightweight.
![]() AWT a très vite été remplacé par Swing tant ses limitations sont nombreuses.
Malgré cette importante abstraction par rapport au système d'exploitation,
Swing a besoin d'AWT pour fonctionner.
Outre le fait que Swing dessine ses composants dans un canevas AWT,
il utilise également son système d'acheminement des événements,
source d'une majeure partie des problèmes de "performances" rencontrés.
Lorsque vous lancez une application Swing, trois threads sont automatiquement créés.
Le premier est le "main application thread" qui se charge d'exécuter la méthode main() de l'application.
Le deuxième thread est le "toolkit thread" dont le rôle est de recevoir les événements du système d'exploitation,
par exemple un clic de souris, et de les transmettre au dernier thread, appelé "event dispatching thread" ou EDT.
Ce dernier est extrêmement important car il répartit les événements reçus vers les composants concernés
et se charge d'invoquer les méthodes d'affichage.
![]() Le système d'exploitation transmet les événements à l'EDT qui les répartit parmi les composants.
Lorsque vous appuyez sur une touche dans un champ texte, l'événement "appui sur une touche"
est transmis à l'instance de JTextField qui met alors à jour son affichage
(par l'entremise de son délégué d'UI).
Toutes ces opérations se déroulent au sein de l'EDT et c'est pour cela que les interfaces
Swing semblent réagir lentement.
Pour vous en convaincre, compilez et exécutez le code du listing 1.
Cliquez sur le bouton "Freeze" dans la fenêtre de l'application pour constater qu'il reste enfoncé.
En effet, lorsque l'utilisateur déclenche une action en appuyant sur le bouton,
la méthode actionPerformed() est exécutée pour mettre en pause le thread courant pendant 4 secondes.
![]() Un bouton qui reste enfoncé, comme en bas à droite, donne l'impression à l'utilisateur que l'application ne fonctionne pas assez vite. Malheureusement, tous les événements sont exécutés dans l'EDT, qui gère également le dessin. En attendant 4 secondes nous bloquons tous les événements et les rafraîchissements de l'interface graphique. Puisque l'EDT fonctionne comme une file d'attente, toute opération un peu longue retarde les autres. Cela prouve que Swing ne souffre pas de mauvaises performances mais que la méconnaissance de son modèle de gestion des threads empêche les développeurs de réaliser des interfaces réactives. ![]() Bloquer l'EDT peut provoquer des artefacts visuels tels que ce rectangle gris. 1. Le modèle de gestion des threads
Le toolkit Swing a été créé en partant du principe que toutes les opérations affectant
l'état des composants seraient réalisées dans l'EDT.
Cela est également vrai pour la création des composants graphiques.
Ainsi, contrairement à ce que l'on a cru pendant des années, une méthode main() telle que celle décrite
dans le listing 1 n'est pas valide et peut entraîner des problèmes d'inter blocage.
La création de la fenêtre et de son contenu devrait avoir lieu dans l'EDT.
Swing n'est donc pas une API "thread safe" et ne doit être manipulée que depuis un seul et unique
thread, l'EDT. Les concepteurs de Swing ont fait ce choix pour garantir la prédictibilité d'exécution
des événements et des rafraîchissements, mais également pour en simplifier l'utilisation et le débogage.
Swing n'est d'ailleurs pas la seule API à fonctionner ainsi : SWT, QT ou encore
les WinForms de .NET fonctionnent sur un modèle de thread unique.
Nous savons à présent que nous devons absolument éviter d'exécuter des opérations longues dans les gestionnaires
d'événement. La solution évidente consiste à placer le code dans un autre thread, comme dans le listing 2.
Dans cet exemple, un nouveau thread est exécuté pour lire un fichier de plusieurs méga-octets et en placer
le contenu dans une JTextArea à l'écran.
A première vue, cet exemple répond à notre problème en débloquant l'EDT.
Malheureusement, il viole la règle du thread unique, puisque nous accédons à un composant Swing depuis un thread
qui n'est pas l'EDT. L'API de Swing offre une solution sous la forme de trois méthodes
de la classe SwingUtilities.
La première s'intitule invokeLater() et permet de poster une tâche dans l'EDT.
Le listing 3 corrige le listing 2 pour s'assurer que la mise à jour de la JTextArea
a lieu dans l'EDT.
Lorsque l'action est déclenchée, l'EDT exécute la méthode actionPerformed().
Cette dernière, pour ne pas bloquer l'EDT, crée un nouveau thread et le démarre.
Dans ce thread, le programme lit un fichier et stocke le résultat dans une chaîne de caractères.
Enfin, il crée une nouvelle tâche, une instance de Runnable, qui est placée à la fin de la file de l'EDT
par invokeLater(). Cette technique doit être employée même lorsque l'opération réalisée par
le gestionnaire d'événement doit avoir lieu dans l'EDT, comme dans l'exemple 4.
Ce code respecte la règle du thread unique mais bloque l'EDT,
le bouton qui a déclenché l'action reste donc enfoncé.
Vous devez donc employer invokeLater() pour corriger ce problème.
Nous avons vu précédemment que la méthode main() du listing 1
est invalide puisqu'elle ne crée pas l'interface graphique dans l'EDT.
Essayez à titre d'exercice de la corriger pour garantir que les composants Swing sont créés dans l'EDT.
La deuxième méthode que vous pouvez utiliser pour gérer convenablement les threads avec Swing
s'appelle isEventDispatchThread(). Elle renvoie vrai lorsque le code s'exécute dans l'EDT.
Vous pouvez ainsi créer des méthodes utilisables depuis l'EDT et depuis un thread quelconque
ainsi que le montre l'exemple 5.
Dans ce code, tickCounter est un entier qui est utilisé pour changer le texte du JLabel counter.
Si incrementLabel() est invoquée dans l'EDT, le code est directement exécuté.
Dans le cas contraire, invokeLater() est appelé pour éviter tout problème.
Cette méthode est donc parfaitement "thread safe" est peut être invoquée depuis un gestionnaire d'événements
comme actionPerformed() ou depuis un thread que vous avez créé.
Le code complet de cet exemple se trouve sur le CD-Rom dans le fichier SwingThreading.java.
![]() La méthode SwingUtilities.isEventDispatchThread() permet d'écrire du code thread safe.
La troisième et dernière méthode indispensable est également la moins utilisée.
Il s'agit d'invokeAndWait() dont le comportement est similaire à invokeLater().
Cette méthode permet d'exécuter du code dans l'EDT et de bloquer le thread courant pendant ce temps.
Prenons le cas d'une application intelligente capable de détecter une latence importante lors de l'ouverture
d'un fichier. Cette application lit le fichier dans un thread et si au bout de 10 secondes l'opération
n'est pas terminée, une boîte de dialogue apparaît pour demander à l'utilisateur s'il souhaite l'annuler
ou la continuer. Pour implémenter cette fonctionnalité, vous devez normalement initialiser un verrou pour
bloquer le thread de lecture puis poster l'affichage de la boîte de dialogue dans l'EDT.
Vous risquez évidemment d'introduire un bug susceptible de provoquer un inter blocage.
Le listing 6 montre comment utiliser invokeAndWait() pour simplifier votre code.
Vous serez sans doute surpris de l'utilisation d'un tableau de 1 entier pour conserver
le résultat de la boîte de dialogue. Souvenez-vous que les classes anonymes ne peuvent
accéder qu'à des membres de classe, des membres d'instance ou à des variables locales finales.
Nous ne pouvons donc pas utiliser un final int puisqu'il ne pourrait alors pas être modifié
lors de l'affection du résultat de showConfirmDialog().
En déclarant un tableau d'entiers (de taille 1) nous pouvons en revanche modifier
l'entier puisque nous ne modifions pas l'objet lui-même, à savoir le tableau.
L'exemple complet se trouve sur le CD-Rom dans le fichier SwingThreadingWait.java.
![]() La méthode invokeAndWait() permet d'interrompre un thread pour attendre le résultat d'une opération effectuée dans l'EDT.
Outre ces méthodes utilitaires, tous les composants Swing possèdent des méthodes parfaitement
thread safe : repaint(), invalidate() et revalidate().
Les deux dernières servent à forcer un composant à réorganiser ses enfants en fonction du layout choisi.
La première méthode, repaint(), force le rafraîchissement de l'affichage d'un composant.
Le listing 7 présente un extrait de l'application SafeRepaint.java présente sur le CD-Rom :
SafeComponent est un composant dérivé de JLabel qui affiche "true"
dans la console si sa méthode d'affichage est exécutée depuis l'EDT.
Le programme SafeRepaint crée un thread qui appelle safeComponent.repaint() toutes les secondes.
En exécutant l'application nous pouvons constater que l'affichage est toujours réalisé depuis l'EDT.
![]() Certaines méthodes Swing, dont repaint(), sont toujours exécutées dans l'EDT.
Sachez enfin que les javax.swing.Timer permettent d'exécuter des actions dans l'EDT
à intervalle régulier.
2. Gestion avancée des tâches
Bien que la classe SwingUtilities permette d'améliorer considérablement
les performances perçues des applications Swing, le code engendré par son utilisation s'avère
difficile à lire et à maintenir. Conscient de cet écueil, les concepteurs de Swing ont créé un nouvel
outil pour gérer facilement les longues opérations, la classe SwingWorker. Celle-ci est à l'heure actuelle
disponible sur Internet, plus particulièrement dans le projet JDNC. La prochaine version 1.6 de Java SE
devrait enfin l'intégrer à l'API officielle.
SwingWorker est une classe abstraite exposant les méthodes construct(), finished(), get(), interrupt() et start().
Pour l'utiliser, nous devons la surcharger et implémenter la méthode abstraite construct().
Une fois le worker créé, vous pouvez lancer le traitement en invoquant la méthode start() qui se chargera de créer un
thread qui exécutera construct(). C'est dans cette dernière que vous devez placer les tâches susceptibles de bloquer
l'EDT. Souvenez-vous que construct() ne s'exécute pas dans l'EDT et que toute mise à jour de l'interface
graphique devra être effectuée à l'aide d'invokeLater() ou d'invokeAndWait().
En examinant la signature de construct(), vous constaterez que vous pouvez retourner une valeur sous forme d'un Object.
Cette valeur peut être récupérée après exécution en invoquant la méthode get().
La plupart du temps vous utiliserez cette valeur dans finished(), appelée après exécution de construct().
Vous pouvez enfin interrompre l'exécution à tout moment à l'aide d'interrupt().
Le listing 8 présente le SwingWorker utilisé par l'application FileFinder.java,
présente sur le CD-Rom.
Ce worker recherche tous les fichiers portant l'extension .java dans le répertoire courant d'exécution.
Lorsque le worker démarre, la liste contenant les résultats est vidée et un message d'attente y est inséré.
Nous modifions la liste dans start() puisque cette méthode est appelée par le gestionnaire d'événements
actionPerformed(), exécuté dans l'EDT.
La méthode construct() génère un tableau contenant la liste de tous les fichiers répondant
à notre critère. A chaque fichier trouvé, le message d'attente est modifié pour indiquer la progression.
Pour cela, nous modifions la liste dans l'EDT. La méthode finished(), exécutée dans l'EDT,
se charge enfin de récupérer les résultats du travail de construct() et de les ajouter à la liste pour
les afficher.
![]() Une méconnaissance de la gestion des threads dans Swing est la cause majeure des problèmes de performances observés.
En exploitant intelligemment l'EDT vous pouvez dès maintenant réaliser des interfaces Swing
aux performances excellentes. Pour vous en convaincre, essayez d'exécuter FileFinder à la racine de
votre disque dur.
![]() Si malgré tout vos applications ne fonctionnent pas assez vite, consultez un ouvrage spécialisé. Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).
|