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 envoyez un fax. Naturellement, à moins que vous ne possédiez
un ordinateur multiprocesseur, le système d'exploitation est obligé
de partager les ressources du processeur, 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.
Il existe deux sortes de multitâches. Le premier, appelé multitâche préemptif, interrompt les programmes sans les consulter. Le second est appelé multitâche coopératif, ou non préemptif chaque programme peut être interrompu lorsqu'il en fournit explicitement l'autorisation. Windows 3.1 est un système d'exploitation coopératif, alors que Windows NT (et Windows 95 pour les programmes 32 bits) est préemptif. Bien que les systèmes d'exploitation soient plus difficiles à implémenter, ils sont bien plus efficaces. Avec un multitâche coopératif, un programme mal conçu peut bloquer le système indéfiniment.
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.

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 rebond() de la classe Balle, qui contient une boucle effectuent 1000 déplacements.


Après chaque déplacement, nous appelons la méthode statique sleep de la classe Thread. 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 on désire la capturer.

Source complet (sans les importations) de la classe Balle
Si vous exécutez ce programme, vous verrez que la balle rebondit très bien, mais qu'elle bloque complètement l'application. Si ces rebonds vous lassent et que vous ne vouliez pas attebdre les 1000 déplacement, cliquez sur le bouton "Fermer". La balle continue quand même son trajet. Il est impossible d'interagir avec le programme tant que la balle rebondit. Remarquez également que le bouton "Démarrer" reste enfoncé pendant tout le déplacement.

Ce genre de situation n'est pas acceptable, ni en théorie ni dans la pratique. C'est également le cas pour la gestion des réseaux. 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 intéressant de pouvoir cliquer sur un bouton "Arrêter" ou "Arrière" pour interrompre le chargement.
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é.
Info : 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. (voir ci-dessous )

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.
Il existe une procédure très simple pour exécuter du code dans un thread séparé : placez le code dans la méthode run d'une classe dérivée de Thread.
Pour exécuter notre programme dans un thread séparé, nous
devons dériver la classe Balle de
la classe Thread et renommer la méthode
rebond par la méthode run,
comme suit :

Lorsque vous construisez un objet dérivé de Thread, le thread créé se place alors en position d'attente. Il convient d'appeler la méthode start dans votre projet pour démarrer réellement le thread. Entre autre chose, la méthode run sera alors automatiquement appelée. (Il ne faut surtout pas appeler directement la méthode run).

Lorsque la méthode run est lancée, le thread est alors exécutable et il le reste jusqu'à la fin de la méthode run. Lorsque cette dernière est complètement exécutée, le thread meurt de lui-même.
Lorsque nous ne plaçons aucune instruction particulière dans la méthode run, le thread rentre en exécution en temps partagé avec les autres threads comme nous l'avons déjà montrés plus haut. Chaque thread effectue alors son travail chacun son tour.
Il existe une autre technique que le temps partagé où le thread de lui-même décide de se mettre en attente (en sommeil). La technique la plus courante pour qu'un thread indique qu'il n'est plus actif est d'appeler la méthode sleep. La méthode run de la classe Balle se sert de sleep(5) pour indiquer que ce thread restera inactif pendant 5 millisecondes. Après ce delai, il sera à nouveau exécuté, mais entre temps, d'autres threads effectuent leurs travaux.
| java.lang.Thread | |
|---|---|
| Thread(); | Construit un nouveau thread. vous devez activer le thread avec start pour activer sa méthode run. |
| void run(); | Cette méthode doit être surchargée et vous devez ajouter le code qui doit être exécuté dans le thread correspondant. |
| void start(); | lance ce thread et appelle sa méthode run. Cette méthode revient immédiatement. Le nouveau thread est exécuté en "même temps". |
| static void sleep(long millis); | place le thread en cours d'exécution en état de veille, pour le nombre spécifié en millisecondes. Notez qu'il s'agit d'une méthode statique. |
| boolean isAlive(); | Renvoie true si le thread est démarré et n'est pas encore terminé. |
| void join(); | Attend que le thread spécifié ait cessé d'être "vivant". |
| void interrupt(); | Envoie une demande d'interruption à ce thread. L'indication "interrompu" du thread est mise à true. Si le thread est actuellement bloqué par un appel à sleep ou à wait, une InterruptedException est déclenchée. |
| static boolean interrupted(); | Regarde si le thread actuel (c'est à dire, le thread qui exécute cette instruction) a été interrompu. Notez qu'il s'agit d'une méthode statique. Cet appel met l'indication "interrompu" du thread courant à false. |
| boolean isInterrupted(); | Regarde si un thread a été interrompu. Contrairement à la méthode statique interrupted, cet appel ne change pas l'indication "interrompu" du thread. |
| Thread(ThreadGroup groupe, String nom); | Crée un nouveau thread qui appartient au groupe de threads spécifié. (voir plus loin). |
| Thread(Runnable cible); | Construit un nouveau thread qui appelle la méthode run de la cible spécifiée. (voir l'interface Runnable en fin de cours). |
Après avoir effectué les changements précisés dans les sources de la rubrique précédente, exécuter le programme. Cliquez maintenant sur le bouton "Démarrer" et pendant que la balle rebondie cliquez sur le bouton "Fermer". Vous remarquez que ce dernier bouton est opérationnel. Relancez l'application et cliquez sur le bouton "Démarrer" plusieurs fois pendant que la balle est en train de rebondir. Vous verrez tout un ensemble de balles dans votre interface graphique. Chaque balle se déplace 1000 fois avant de s'arrêter.

Cet exemple met en évidence un grand avantage de l'architecture des threads dans le langage Java. Il est très facile de créer un nombre quelconque d'objets autonomes qui semblent être exécutés en parallèle.
Les threads peuvent se trouver dans l'un des quatre états suivants :

Lorsque vous créez un thread avec l'opérateur new ( par exemple new Balle(panneau) ), le thread n'est pas encore exécuté. Cela signifie qu'il se trouve dans l'état Nouveau. Lorsqu'un thread se trouve dans cet état, le programme n'a pas encore commencé à exécuter le code qui se trouve dans le thread. Il faut faire encore quelques opérations avant que ce thread puisse être exécuté. Ces opérations et l'allocation des ressources nécessaires reviennent à la méthode start.
Une fois que vous avez invoqué la méthode start, le thread devient Exécutable. Il se peut que le thread exécutable ne soit pas encore en cours d'exécution. C'est au système d'exploitation de fournir au thread une fenêtre d'exécution. Lorsque le code à l'intérieur d'un thread commence à être exécuté, ce thread est en cours d'exécution. Un thread en cours d'exécution reste dans l'état Exécutable. 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. N'oubliez jamais qu'un thread exécutable peut être ou non en cours d'exécution.
Un thread entre dans l'état Bloqué lorsque l'une des actions suivantes se produit :
Pour sortir de l'état bloqué et entrer à nouveau dans l'état exécutable, un thread doit emprunter le chemin inverse de celui qu'il a suivi pour devenir bloqué.
Un thread peut mourrir pour l'une des deux raisons suivantes :
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.
Un thread s'arrête naturellement lorsque sa méthode run est terminée. In n'existe pas de méthode valable pour mettre fin à un thread. Cela signifie que la méthode run d'un thread doit vérifier régulièrement si elle doit être terminée.

Cependant, un thread ne fonctionne pas en permanence, mais il doit se rendormir ou attendre régulièrement, pour donner aux autres threads une chance d'être exécutés. Mais lorsqu'un thread dort, il ne peut pas déterminer s'il doit s'arrêter. C'est à ce moment que la méthode interrupt intervient. Lorsque la méthode 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 en plaçant des actions appropriées dans la clause catch qui est en relation avec InterruptedException.

Cependant, cette architecture du code pose un problème. Si la méthode interrupt a été appelée pendant que le thread n'était pas en sommeil ou en attente, aucune InterruptedException n'a été générée. Le thread doit appeler la méthode interrupted pour déterminer s'il a été récemment interrompu.

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. Elle sera créée lorsque le bouton "Démarrer" sera activé et elle cessera d'exister lorsqu'on cliquera sur le bouton "Arrêter".

Attention cette fois-ci l'objet balle doit être un attribut de la classe Cadre et non plus être déclarée à l'intérieur du gestionnaire du bouton "Démarrer". Par ailleurs, si on ne demande pas d'arrêter, la balle rebondie indéfiniment. Voici les différents sources correspondant.

Déclaration de balle

Gestionnaire des boutons "Démarrer" et "Arrêter"

La méthode run de la classe Balle
Certains programmes contiennent un nombre important de threads. Il devient alors utile de les regrouper par fonctionnalités. Par exemple, considérons un navigateur Internet. Si plusieurs threads essaient d'obtenir des images à partir d'un serveur, et que l'utilisateur clique sur le bouton "Arrêter" pour interrompre le chargement de la page courante, il est alors pratique de disposer d'un moyen d'interrompre tous ces threads simultanément. Java vous permet de construire ce qui s'appelle un groupe de threads pour que vous puissiez travailler simultanément avec plusieurs threads. Vous pouvez construire un groupe de threads à l'aide de la classe ThreadGroup.

La chaîne passée en argument du constructeur ThreadGroup sert à identifier le groupe et doit être unique. Vous pouvez alors ajouter des threads au groupe de threads en spécifiant ce dernier dans le constructeur du thread.
Pour interrompre tous les threads d'un groupe de threads, appelez simplement la méthode interrupt sur l'objet du groupe.
| java.lang.ThreadGroup | |
|---|---|
| ThreadGroup(String nom); | Construit un nouveau groupe de threads identifié par la chaîne nom |
| int activeCount(); | Cette méthode renvoie la borne supérieure correspondant au nombre de threads actifs dans le groupe de threads. |
| void interrupt(); | Interrompt tous les threads de ce groupe de threads. |
Reprenons le programme précédent en prenant en compte le rebond de plusieurs balles. Comme auparavant, chaque balle est repésentée par un thread. Le fait de cliquer sur le bouton "Arrêter" doit pouvoir enlever toutes les balles présentes sur l'interface graphique. Il faut donc utiliser la notion de groupe comme nous venons de le découvrir.

Dans ce programme le groupe de thread doit être un attribut de la classe Cadre. Lorsque vous cliquez sur le bouton "Démarrer", vous créez une nouvelle balle par rapport à ce groupe, ce qui nécessite par ailleurs de redéfinir le constructeur de la classe Balle. Lorsque vous cliquez sur le bouton "Arrêter" vous interrompez tout simplement le groupe.
Voici donc les petites modifications à apporter pour obtenir ce comportement :

Déclaration de groupe

Gestionnaire des boutons "Démarrer" et "Arrêter"

Nouveau constructeur de la classe Balle
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.
Pour éviter que plusieurs threads accèdent à un même objet partagé de façon anarchique, vous devez apprendre comment synchroniser cet accès. Dans cette section nous allons voir ce qui se passe si nous n'utilisons pas de synchronisation.

Observons le code ci-dessus. Il y figure :

Après le travail des banquiers, le capital n'est que de 1 : il y a un manque de synchronisation entre les banquiers. Successivement :

Le code précédent pourrait être plus simple, mais il a été conçu pour forcer l'absence de synchronisation.
Le problème du programme précédent est que la méthode crédite peut être interrompue en plein milieu de son exécution. Si nous pouvions être sûr que cette méthode soit entièrement terminée avant que son thread soit interrompu, l'état de l'objet compte ne serait pas corrompu.
Le langage Java permet d'empêcher qu'une méthode soit interrompue. Il suffit alors de placer le terme synchronized devant la méthode concernée. On dit alors que la méthode est synchronisée.

Lorsqu'un thread appelle une méthode synchronisée, il peut être assuré que cette méthode sera terminée avant qu'un autre thread puisse exécuter une méthode synchronisée sur le même objet. Lorsqu'un thread appelle crédite et qu'un autre thread appelle également crédite, le second thread ne peut pas continuer. Il est alors désactivé et doit attendre la fin de l'exécution du premier thread pour appeler la méthode crédite.

voici le résultat

Lorsqu'un thread appelle une méthode synchronisée, l'objet devient "vérouillé". Vous pouvez considérer que chaque objet possède une clé ouvrant sur une porte d'accès à cet objet. Cette clé se trouve initialement devant la porte. Lorsqu'un thread entre dans la méthode synchronisée, il ramasse la clé, entre dans l'objet et referme la porte de l'intérieur. Lorsqu'un autre thread essaie d'appeler une méthode synchronisée sur le même objet, il ne peut pas ouvrir la porte. Il cherche alors la clé et ne peut la trouver, donc il suspend son exécution. Eventuellement, le premier thread sort de sa méthode synchronisée, quitte l'objet et dépose la clé devant la porte.
Périodiquement, le gestionnaire de threads active les threads qui attendent une clé. Lorsqu'un des threads qui souhaite utiliser l'objet est à nouveau en exécution, il regarde si l'objet est toujours vérouillé. Dans le cas, il devient le prochain thread à bénéficier d'un accès exclusif à cet objet.
Ce système de verrou parait très intéressant, mais il faut être très attentif. Imaginons que dans la méthode crédite nous ayons un tranfert vers une base de données. Imaginons maintenant qu'un thread appelle cette méthode, et que pour une raison quelconque, la base de données soit défaillante. La méthode est alors bloquée et le thread avec elle, mais le plus grave est que la clé n'a pas été redonnée, ce qui signifie que, éventuellement, tous les autres threads sont bloqués. En fait tout le système est bloqué. Il existe des méthodes pour pallier à ce problème comme les méthodes wait, notify et notifyAll, toutefois, nous ne ferons pas l'étude de cette particularité.
Dans les sections précédentes, nous avons appris les éléments nécessaires pour transformer un programme en plusieurs tâches concurrentes. Chaque tâche doit se trouver dans la méthode run d'une classe qui étend Thread. Mais que se passe-t-il si nous voulons placer la méthode run dans une classe qui étend déjà une autre classe ? Cela se produit, par exemple, lorsque nous voulons ajouter une animation à une classe Nom qui hérite déjà de JLabel. Nous ne pouvons pas hériter de deux classes parents, et nous devons donc passer par une interface qui s'appelle Runnable. Cette interface dispose de la méthode run que vous devez absolument redéfinir (principe même d'une interface).

Lorsque vous devez utiliser du multithread dans une classe qui dérive déjà d'une autre classe que Thread, vous pouvez demander à cette classe d'implémenter l'interface Runnable. Comme si vous l'aviez dérivée de Thread, placez le code à exécuter dans la méthode run.

Vous devrez encore créer un objet thread pour exécuter le thread. Fournissez à ce thread une référence vers l'objet Runnable dans son constructeur. Le thread appelle alors la méthode run de cet objet.

Dans ce cas, l'argument nom du constructeur Thread spécifie que l'objet dont la méthode run doit être appelée lorsque le thread est exécuté est une instance de l'objet Nom.
| java.lang.Runnable | |
|---|---|
| void run(); | Vous devez surcharger cette méthode et placer dans le thread le code que vous voulez exécuter. |
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.
| java.lang.Process | |
|---|---|
| InputStream getInputStream(); | Récupère le flot de données à lire depuis l'autre programme |
| OutputStream getOutputStream(); | Récupère le flot de données à envoyer vers l'autre programme |
| int waitFor() throws InterruptedException; | Le programme actuel se met en attente jusqu'à ce que le processus demandé s'arrête. |
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.
| java.lang.Runtime | |
|---|---|
| static Runtime getRuntime(); | Retourne l'objet Runtime de la plate-forme actuelle ; cet objet peut exécuter des fonctions système d'une manière indépendante de la plate-forme. |
| Process exec(String commande) throws IOException; | Démarre un nouveau processus s'exécutant dans un environnement externe à l'interpréteur. Notez que tous les processus qui s'exécutent hors de l'environnement Java peuvent être dépendants du système. |
| long freeMemory(); | Retourne la quantité approximative de la mémoire libre. |
| long totalMemory(); | Retourne la quantité totale de mémoire disponible au sein de l'interpréteur Java. |
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.
Swing possède une classe chronomètre intégrée (Timer) qui est facile à utiliser. Vous pouvez construire un chronomètre en fournissant un objet d'une classe qui implémente l'interface ActionListener (permet de préciser l'objet qui est en écoute d'un événement particulier comme ici le temps écoulé), et le délai entre deux alertes du chronomètre, en millisecondes.

Ensuite la méthode actionPerformed de la classe de l'écouteur est appelée automatiquement sur le thread de répartition des événements et ainsi vous pouvez préciser ce que vous désirez faire à chaque top d'horloge.

| javax.swing.Timer | |
|---|---|
| Timer(int délai, ActionListener écouteur); | Crée un nouveau chronomètre qui envoie des événement temporels à un écouteur. délai : le délai, en milliseconde, entre deux événement. écouteur : écouteur d'action qui doit être averti lorsque le délai est écoulé. |
| void start(); | Lance le chronomètre. Après cet appel, le chronomètre commence à envoyer des événements à son écouteur d'action. |
| void stop(); | Arrête le chronomètre. Après cet appel, le chronomètre arrête d'envoyer des événements à son écouteur d'action. |
| void setInitialDelay(int délai); | spécifie le temps en millisecondes entre l'invocation de la méthode start et le déclenchement du premier ActionEvent. |
| void setRepeats(boolean plusieurs); | spécifie si le timer doit envoyer un événement périodiquement. Si plusieurs = false, un seul événement est envoyer après l'appel de la méthode start. Par défaut, plusieurs = true. |
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.


![]()