Thread
ou l'interface
Runnable
et leurs fonctionnements, mais d'un enrichissement, qui étend ces anciennes fonctionnalités, tout en les conservant.
Il faut donc examiner la première pour comprendre le fonctionnement de la seconde. Écrivons donc un premier
thread
, à l'ancienne, comme on le faisait au bon vieux temps de Java 1.1.
Un thread en Java est un objet, qui doit étendre la classe
Thread
. Comme Java ne supporte pas l'héritage multiple, il n'est pas toujours possible d'étendre cette classe. Java nous propose donc aussi un mécanisme d'enveloppe (
wrapper
), que nous allons voir.
Thread
(package
java.lang
). Nous devons surcharger une seule méthode : la méthode
run()
, qui retourne
void
, et ne prend aucun paramètre. Cette méthode ne jette non plus aucune exception, et nous verrons que cela pose en fait un problème.
Exemple 54. Premier
thread
par extension de la classe
Thread
public class Bateau extends Thread { // surcharge de la méthode run() de la classe Thread public void run() { int n = 0 ; while (n++ < 100) { System.out.println("Je vogue !") ; try { Thread.sleep(10) ; } catch (InterruptedException e) { // gestion de l'erreur } } } }
run()
de ce thread.
Voyons comment lancer ce thread.
Exemple 55. Lancement d'un premier thread
// dans une méthode main() // instanciantion d'un objet de type Thread Bateau b = new Bateau() ; // lancement de ce thread par appel à sa méthode start() b.start() ; // cette méthode rend immédiatement la main... System.out.println("Thread lancé") ; // ... mais la méthode main() ne quitte pas immédiatement
start()
de l'instance de
Thread
que l'on veut exécuter. Notons ici qu'il s'agit bien de la méthode
start()
, et non pas de la méthode
run()
, que l'on a surchargée. Appeler la méthode
run()
au lieu de
start()
permet d'exécuter le code de ce
thread
, mais dans le fil d'exécution courant. Aucun nouveau fil n'est créé dans ce cas, ce n'est en général pas ce que l'on veut ! Appeler
run()
à la place de
start()
est une erreur que l'on rencontre parfois.
L'appel à cette méthode
start()
a pour effet de créer un nouveau fil d'exécution dans la machine Java. Ce fil d'exécution va exécuter la méthode
run()
de cet objet. C'est pourquoi l'on surcharge cette méthode.
Dans le cas de notre exemple, l'appel à la méthode
b.start()
rend la main immédiatement. On voit donc s'afficher dans la console, immédiatement, le message
Thread lancé
. Contrairement à l'exécution d'un programme classique, la méthode
main()
ne quitte pas une fois son exécution terminée. Une fois toutes les 10 millisecondes, on voit le message
Je vogue !
s'afficher. Une fois qu'il s'est affiché 100 fois, alors notre application exemple se termine.
Que se passe-t-il lorsque l'on appelle par erreur la méthode
run()
plutôt que la méthode
start()
? La méthode
run()
va s'exécuter, mais dans le thread courant, puisque seul l'appel à
start()
déclenche la création d'un nouveau thread au niveau de la machine Java. On pourra avoir l'impression que tout se passe normalement, puisque les traitements seront tout de même effectués. Cela dit, comme tout se passe dans le même fil d'exécution, l'ordre des affichages n'est pas le même. Les messages
Je vogue !
s'affichent tout d'abord. Suit enfin le message
Thread lancé
, prometteur, mais erroné !
D'une façon générale, une application qui lance des
threads
ne peut pas quitter normalement tant que tous ces
threads
sont encore actifs. Il faut donc les arrêter, nous verrons comment. Un type de
thread
particulier, appelé
daemon
ne présente pas cette contrainte.
Runnable
. Cette interface n'expose qu'une unique méthode :
run()
.
Exemple 56. Deuxième thread, par implémentation de
Runnable
public class AutreBateau implements Runnable { // implémentation de la méthode run() de l'interface Runnable public void run() { int n = 0 ; while (n++ < 100) { System.out.println("Je vogue aussi !") ; try { Thread.sleep(10) ; } catch (InterruptedException e) { // gestion de l'erreur } } } }
Runnable
au lieu d'étendre
Thread
.
La façon dont on lance un tel
thread
, est en revanche fondamentalement différente. Une instance de cette classe doit être passée en paramètre du constructeur d'une nouvelle instance de
Thread
, et c'est sur ce nouveau thread que nous allons invoquer la méthode
start()
.
Exemple 57. Lancement d'un deuxième thread
// dans une méthode main() // instanciantion d'un objet de type Runnable AutreBateau autreBateau = new AutreBateau() ; // construction d'un Thread en passant cette instance de Runnable en paramètre Thread thread = new Thread(autreBateau) ; // lancement de ce thread par appel à sa méthode start() thread.start() ; // cette méthode rend immédiatement la main... System.out.println("Thread lancé") ; // ... mais la méthode main() ne quitte immédiatement
Thread
que l'on crée ici est complètement générique. Appeler sa méthode
start()
demande toujours à la machine Java de créer un nouveau fil d'exécution, et le fonctionnement interne de la classe
Thread
fait que ce fil va exécuter la méthode
run()
de l'objet passé en paramètre lors de la construction de notre instance de
Thread
.
Thread.sleep(long)
dans nos deux exemples, sans expliquer son fonctionnement.
Le
long
passé en paramètre correspond au nombre de millisecondes que cette méthode doit attendre pour rendre la main. Elle jette une exception de type
InterruptedException
, que l'on doit gérer de manière standard. Ici nous avons choisi de capter cette exception.
Il existe également une méthode
wait(long)
sur la classe
Object
. Elle fait la même chose, mais son fonctionnement interne est très différent. On évitera de l'utiliser, et on lui préfèrera la méthode
Thread.sleep(long)
.
Nous verrons dans la suite pourquoi cette exception est jetée.
stop()
. Cette méthode est dépréciée, et ne doit plus être utilisée.
On ne peut pas, dans le cadre de l'API Concurrent, arrêter un
thread
de l'extérieur, de façon autoritaire. La bonne façon d'arrêter un
thread
, est de positionner un champ particulier de ce
thread
. Il est alors de la responsabilité du
thread
de vérifier périodiquement la valeur de ce champ, et lorsqu'elle lui indique qu'il faut qu'il s'interrompe, c'est à lui de le faire.
Cette approche est valide dans la plupart des cas, mais pas dans le cas où le
thread
qui doit s'arrêter, est lui-même en attente du retour d'une opération bloquante, ou dans un état d'attente (méthodes
Object.wait(long millis)
ou
Thread.sleep(long millis
). Dans ce cas, on peut interrompre le
thread
. Ces méthodes d'attente sont en général capables de détecter cette interruption, et jettent alors une exception de type
InterruptedException
.
Exemple 58. Interruption d'un thread
// dans une méthode main Runnable r = new Runnable() { public void run() { try { while (!Thread.currentThread().isInterrupted()){ // traitements ... // petite sieste Thread.sleep(5000) ; } } catch (InterruptedException e) { // nous avons été interrompu // on remet interrupted à false par l'appel à cette méthode Thread.currentThread().interrupted() ; } } public void cancel() { // interruption du thread courant, c'est-à-dire le nôtre Thread.currentThread().interrupt() ; } }
cancel()
.
Notons le comportement d'un
thread
interrompu.
true
de la propriété
interrupted
;
interrupted
à
false
, et jette une exception de type
InterruptedException
;
interrupted
est laissé à
true
. Sa valeur peut être testée par appel à
Thread.currentThread().isInterrupted()
. Il est de la responsabilité du traitement de s'interrompre alors.