Threads et performance avec Swing

Romain Guy (Gfx)

Le toolkit Swing permet aux développeurs Java de réaliser des applications graphiques très complexes. Sa complexité rend malheureusement aisée la réalisation d'interfaces présentant de piètres performances. ♪

- Titre : Threads et performance avec Swing

- Auteur : Romain Guy (Gfx)

- Parution : 19 juin 2006

- Licence : Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).

Sommaire

- Introduction

- I. Le modèle de gestion des threads

- II. Gestion avancée des tâches

Introduction

Note de la rédaction : ce tutoriel avait été publié à l'origine dans le défunt magazine Login. Les fichiers du CD-Rom de cet article sont disponibles en téléchargement en archive ZIP (cdrom.zip).

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.

listing 1

public class FreezeEDT extends JFrame implements ActionListener {
public FreezeEDT() {
super("Freeze");
JButton freezer = new JButton("Freeze");
freezer.addActionListener(this);
add(freezer);
pack();
}

public void actionPerformed(ActionEvent e) {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
}
}

public static void main(String... args) {
FreezeEDT edt = new FreezeEDT();
edt.setVisible(true);
}
}

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.

I - 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.

listing 2

public void actionPerformed(ActionEvent e) {
new Thread(new Runnable() {
public void run() {
String text = readHugeFile();
textArea.setText(text);
}
}).start();
}

Dans cet exemple, un nouveau thread est exécuté pour lire un fichier de plusieurs mégaoctets et en placer le contenu dans une JTextArea à l'écran. À 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.

listing 3

public void actionPerformed(ActionEvent e) {
new Thread(new Runnable() {
public void run() {
final String text = readHugeFile();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
textArea.setText(text);
}
}
}
}).start();
}

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.

listing 4

public void actionPerformed(ActionEvent e) {
JFrame f = new JFrame();
f.getContentPane().setLayout(new FlowLayout());
for (int i = 0; i < 5000; i++) {
f.add(new JLabel("Label " + i));
}
f.pack();
f.setVisible(true);
}

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.

listing 5

private void incrementLabel() {
tickCounter++;
Runnable code = new Runnable() {
public void run() {
counter.setText(String.valueOf(tickCounter));
}
};

if (SwingUtilities.isEventDispatchThread()) {
code.run();
} else {
SwingUtilities.invokeLater(code);
}
}

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.

listing 6

try {
final int[] answer = new int[1];
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
answer[0] = JOptionPane.showConfirmDialog(SwingThreadingWait.this,
"Abort long operation?",
"Abort?",
JOptionPane.YES_NO_OPTION);
}
});
if (answer[0] == JOptionPane.YES_OPTION) {
return;
}
} catch (InterruptedException ie) {
} catch (InvocationTargetException ite) {
}

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 :

listing 7

public class SafeComponent extends JLabel {
public SafeComponent() {
super("Safe Repaint");
}

public void paintComponent(Graphics g) {
super.paintComponent(g);
System.out.println(SwingUtilities.isEventDispatchThread());
}
}

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.

II - 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. Conscients 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.

Note de la rédaction : La classe SwingWorker sera bien présente dans Java SE 6. Mais, en comparaison de la classe indiquée dans cette page, elle a été grandement remaniée afin de profiter des nouveautés du langage apportées par Java 5.0 (notamment les Generics).

Toutefois le principe général reste le même…

Pour plus d'informations, veuillez consulter la documentation officielle de la classe :

http://java.sun.com/javase/6/docs/api/javax/swing/SwingWorker.html

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.

listing 8

public class FileFinderWorker extends SwingWorker {
private JList filesList;
private DefaultListModel filesListModel;

public FileFinderWorker(JList filesList) {
this.filesList = filesList;
this.filesListModel = new DefaultListModel();
this.filesList.setModel(this.filesListModel);
}

public void start() {
filesListModel.removeAllElements();
filesListModel.addElement("Searching *.java...");
super.start();
}

public void finished() {
filesListModel.removeAllElements();
String[] files = (String[]) get();
for (String file: files) {
filesListModel.addElement(file);
}
}

public Object construct() {
FileFilter filter = new FileFilter() {
public boolean accept(File pathname) {
return pathname.getName().endsWith(".java");
}
};

final List list = new ArrayList();
final int[] counter = new int[1];

try {
FileTreeWalker walker = new FileTreeWalker(".", filter);
walker.walk(new FileTreeWalk() {
public void walk(File path) {
list.add(path.toString());
counter[0]++;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
filesListModel.setElementAt("Searching *.java... (" + counter[0] + ")", 0);
}
});
}
});
} catch (IOException e) {
}

return list.toArray(new String[list.size()]);
}
}

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. À 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é.