Java 101: Comprendre les threads Java, Partie 1: Présentation des threads et des exécutables

Cet article est le premier d'une série Java 101 en quatre parties explorant les threads Java. Bien que vous puissiez penser que le threading en Java serait difficile à comprendre, j'ai l'intention de vous montrer que les threads sont faciles à comprendre. Dans cet article, je vous présente les threads et les exécutables Java. Dans les articles suivants, nous explorerons la synchronisation (via des verrous), les problèmes de synchronisation (tels que le blocage), le mécanisme d'attente / notification, la planification (avec et sans priorité), l'interruption des threads, les minuteries, la volatilité, les groupes de threads et les variables locales des threads .

Notez que cet article (qui fait partie des archives JavaWorld) a été mis à jour avec de nouvelles listes de codes et du code source téléchargeable en mai 2013.

Comprendre les threads Java - lire toute la série

  • Partie 1: Présentation des threads et des exécutables
  • Partie 2: Synchronisation
  • Partie 3: Planification des threads et attente / notification
  • Partie 4: Groupes de threads et volatilité

Qu'est-ce qu'un fil?

Conceptuellement, la notion de thread n'est pas difficile à appréhender: c'est un chemin d'exécution indépendant à travers le code du programme. Lorsque plusieurs threads s'exécutent, le chemin d'un thread à travers le même code diffère généralement des autres. Par exemple, supposons qu'un thread exécute le code d'octet équivalent d'une partie d'une instruction if-else if, tandis qu'un autre thread exécute le code d'octet équivalent de la elsepartie. Comment la JVM assure-t-elle le suivi de l'exécution de chaque thread? La machine virtuelle Java attribue à chaque thread sa propre pile d'appels de méthode. Outre le suivi de l'instruction de code d'octet actuelle, la pile d'appels de méthode suit les variables locales, les paramètres que la JVM transmet à une méthode et la valeur de retour de la méthode.

Lorsque plusieurs threads exécutent des séquences d'instructions de code d'octet dans le même programme, cette action est appelée multithreading . Le multithreading profite à un programme de différentes manières:

  • Les programmes basés sur une interface utilisateur graphique (interface utilisateur graphique) multithread restent réactifs aux utilisateurs lors de l'exécution d'autres tâches, telles que la repagination ou l'impression d'un document.
  • Les programmes filetés se terminent généralement plus rapidement que leurs homologues non filetés. Cela est particulièrement vrai des threads exécutés sur une machine multiprocesseur, où chaque thread a son propre processeur.

Java effectue le multithreading via sa java.lang.Threadclasse. Chaque Threadobjet décrit un seul thread d'exécution. Cette exécution se produit dans Threadla run()méthode de. Étant donné que la run()méthode par défaut ne fait rien, vous devez effectuer une sous Thread- classe et une substitution run()pour accomplir un travail utile. Pour un avant-goût des threads et du multithreading dans le contexte de Thread, examinez le Listing 1:

Liste 1. ThreadDemo.java

// ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i < 50; i++) System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print ('*'); System.out.print ('\n'); } } }

Le listing 1 présente le code source à une application composée de classes ThreadDemoet MyThread. La classe ThreadDemopilote l'application en créant un MyThreadobjet, en démarrant un thread qui s'associe à cet objet et en exécutant du code pour imprimer une table de carrés. En revanche, MyThreadremplace Threadla run()méthode d 'impression (sur le flux de sortie standard) d' un triangle à angle droit composé de caractères astérisque.

Planification des threads et JVM

La plupart des implémentations JVM (sinon toutes) utilisent les capacités de thread de la plate-forme sous-jacente. Étant donné que ces fonctionnalités sont spécifiques à la plate-forme, l'ordre de sortie de vos programmes multithread peut différer de l'ordre de sortie de quelqu'un d'autre. Cette différence résulte de la planification, un sujet que j'explorerai plus tard dans cette série.

Lorsque vous tapez java ThreadDemopour exécuter l'application, la JVM crée un thread d'exécution de départ, qui exécute la main()méthode. En s'exécutant mt.start ();, le thread de départ indique à la JVM de créer un second thread d'exécution qui exécute les instructions de code d'octet comprenant la méthode de l' MyThreadobjet run(). Lorsque la start()méthode retourne, le thread de départ exécute sa forboucle pour imprimer une table de carrés, tandis que le nouveau thread exécute la run()méthode pour imprimer le triangle rectangle.

À quoi ressemble la sortie? Courez ThreadDemopour le découvrir. Vous remarquerez que la sortie de chaque thread a tendance à s'intercaler avec la sortie de l'autre. Cela résulte du fait que les deux threads envoient leur sortie vers le même flux de sortie standard.

La classe Thread

Pour développer vos compétences en écriture de code multithread, vous devez d'abord comprendre les différentes méthodes qui composent la Threadclasse. Cette section explore plusieurs de ces méthodes. Plus précisément, vous découvrez les méthodes pour démarrer les threads, nommer les threads, mettre les threads en veille, déterminer si un thread est actif, joindre un thread à un autre thread et énumérer tous les threads actifs dans le groupe et les sous-groupes de threads actuels. Je discute également Threaddes aides au débogage et des threads utilisateur par rapport aux threads démons.

Je présenterai le reste des Threadméthodes de 's dans les articles suivants, à l'exception des méthodes obsolètes de Sun.

Méthodes obsolètes

Sun a déconseillé diverses Threadméthodes, telles que suspend()et resume(), car elles peuvent verrouiller vos programmes ou endommager des objets. Par conséquent, vous ne devez pas les appeler dans votre code. Consultez la documentation du SDK pour les solutions de contournement à ces méthodes. Je ne couvre pas les méthodes obsolètes dans cette série.

Construire des threads

Threada huit constructeurs. Les plus simples sont:

  • Thread(), qui crée un Threadobjet avec un nom par défaut
  • Thread(String name), qui crée un Threadobjet avec un nom que l' nameargument spécifie

Les constructeurs les plus simples suivants sont Thread(Runnable target)et Thread(Runnable target, String name). Hormis les Runnableparamètres, ces constructeurs sont identiques aux constructeurs mentionnés ci-dessus. La différence: les Runnableparamètres identifient les objets extérieurs Threadqui fournissent les run()méthodes. (Vous apprenez Runnableplus tard dans cet article.) Les quatre derniers constructeurs ressemblent Thread(String name), Thread(Runnable target)et Thread(Runnable target, String name); cependant, les constructeurs finaux incluent également un ThreadGroupargument à des fins organisationnelles.

L'un des quatre derniers constructeurs Thread(ThreadGroup group, Runnable target, String name, long stackSize),, est intéressant en ce qu'il vous permet de spécifier la taille souhaitée de la pile d'appels de méthode du thread. Être capable de spécifier cette taille s'avère utile dans les programmes avec des méthodes qui utilisent la récursivité - une technique d'exécution par laquelle une méthode s'appelle à plusieurs reprises - pour résoudre avec élégance certains problèmes. En définissant explicitement la taille de la pile, vous pouvez parfois empêcher StackOverflowErrors. Cependant, une taille trop grande peut entraîner OutOfMemoryErrors. De plus, Sun considère que la taille de la pile des appels de méthode dépend de la plate-forme. Selon la plate-forme, la taille de la pile des appels de méthode peut changer. Par conséquent, réfléchissez bien aux ramifications de votre programme avant d'écrire du code qui appelle Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Démarrez vos véhicules

Les threads ressemblent à des véhicules: ils déplacent les programmes du début à la fin. Threadet les Threadobjets de sous - classe ne sont pas des threads. Au lieu de cela, ils décrivent les attributs d'un thread, tels que son nom, et contiennent du code (via une run()méthode) que le thread exécute. Lorsque vient le temps d'exécuter un nouveau thread run(), un autre thread appelle la méthode de Threadl'objet 's ou sa sous-classe start(). Par exemple, pour démarrer un deuxième thread, le thread de départ de l'application - qui s'exécute - main()appelle start(). En réponse, le code de gestion des threads de la JVM fonctionne avec la plate-forme pour garantir que le thread s'initialise correctement et appelle Threadla run()méthode d'un objet ou de sa sous-classe .

Une fois start()terminé, plusieurs threads s'exécutent. Parce que nous avons tendance à penser de manière linéaire, nous avons souvent du mal à comprendre l'activité simultanée (simultanée) qui se produit lorsque deux ou plusieurs threads sont en cours d'exécution. Par conséquent, vous devez examiner un graphique qui montre où un thread s'exécute (sa position) en fonction du temps. La figure ci-dessous présente un tel graphique.

Le graphique montre plusieurs périodes significatives:

  • L'initialisation du thread de départ
  • Le moment où ce thread commence à s'exécuter main()
  • Le moment où ce thread commence à s'exécuter start()
  • Le moment start()crée un nouveau fil et revient àmain()
  • L'initialisation du nouveau thread
  • Le moment où le nouveau thread commence à s'exécuter run()
  • Les différents moments où chaque thread se termine

Notez que l'initialisation du nouveau thread, son exécution run()et sa terminaison se produisent simultanément avec l'exécution du thread de départ. Notez également qu'après un appel de thread start(), les appels suivants à cette méthode avant la fin de la run()méthode provoquent la start()levée d'un java.lang.IllegalThreadStateExceptionobjet.

Qu'est-ce qu'il y a dans un nom?

Pendant une session de débogage, distinguer un thread d'un autre de manière conviviale s'avère utile. Pour différencier les threads, Java associe un nom à un thread. Ce nom est par défaut Threadun caractère trait d'union et un nombre entier de base zéro. Vous pouvez accepter les noms de thread par défaut de Java ou choisir les vôtres. Pour accueillir les noms personnalisés, Threadfournit des constructeurs qui acceptent des namearguments et une setName(String name)méthode. Threadfournit également une getName()méthode qui renvoie le nom actuel. Le listing 2 montre comment établir un nom personnalisé via le Thread(String name)constructeur et récupérer le nom actuel dans la run()méthode en appelant getName():

Liste 2. NameThatThread.java

// NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { // The compiler creates the byte code equivalent of super (); } MyThread (String name) { super (name); // Pass name to Thread superclass } public void run () { System.out.println ("My name is: " + getName ()); } }

Vous pouvez passer un argument de nom facultatif à MyThreadsur la ligne de commande. Par exemple, java NameThatThread Xétablit le Xnom du thread. Si vous ne parvenez pas à spécifier un nom, vous verrez la sortie suivante:

My name is: Thread-1

Si vous préférez, vous pouvez changer l' super (name);appel dans le MyThread (String name)constructeur en un appel à setName (String name)— comme dans setName (name);. Ce dernier appel de méthode atteint le même objectif - établir le nom du thread - que super (name);. Je laisse cela comme un exercice pour vous.

Nommer principal

Java attribue le nom mainau thread qui exécute la main()méthode, le thread de départ. Vous voyez généralement ce nom dans le Exception in thread "main"message que le gestionnaire d'exceptions par défaut de la machine virtuelle Java imprime lorsque le thread de départ lève un objet d'exception.

Dormir ou ne pas dormir

Later in this column, I will introduce you to animation— repeatedly drawing on one surface images that slightly differ from each other to achieve a movement illusion. To accomplish animation, a thread must pause during its display of two consecutive images. Calling Thread's static sleep(long millis) method forces a thread to pause for millis milliseconds. Another thread could possibly interrupt the sleeping thread. If that happens, the sleeping thread awakes and throws an InterruptedException object from the sleep(long millis) method. As a result, code that calls sleep(long millis) must appear within a try block—or the code's method must include InterruptedException in its throws clause.

Pour démontrer sleep(long millis), j'ai écrit une CalcPI1application. Cette application démarre un nouveau thread qui utilise un algorithme mathématique pour calculer la valeur de la constante mathématique pi. Pendant que le nouveau thread calcule, le thread de départ s'interrompt pendant 10 millisecondes en appelant sleep(long millis). Une fois le thread de départ éveillé, il imprime la valeur pi, que le nouveau thread stocke dans variable pi. Le listing 3 présente CalcPI1le code source de:

Liste 3. CalcPI1.java

// CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); // Sleep for 10 milliseconds } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run () { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } }

Si vous exécutez ce programme, vous verrez une sortie similaire (mais probablement pas identique) à ce qui suit:

pi = -0.2146197014017295 Finished calculating PI