Threading moderne: une introduction à la concurrence Java

Une grande partie de ce qu'il y a à apprendre sur la programmation avec des threads Java n'a pas radicalement changé au cours de l'évolution de la plate-forme Java, mais elle a changé progressivement. Dans cette introduction aux threads Java, Cameron Laird atteint certains des points hauts (et bas) des threads en tant que technique de programmation simultanée. Obtenez un aperçu de ce qui est toujours difficile dans la programmation multithread et découvrez comment la plate-forme Java a évolué pour relever certains des défis.

La concurrence est l'un des plus grands soucis des nouveaux arrivants dans la programmation Java, mais il n'y a aucune raison de la laisser vous décourager. Non seulement une excellente documentation est disponible (nous explorerons plusieurs sources dans cet article), mais les threads Java sont devenus plus faciles à utiliser à mesure que la plate-forme Java a évolué. Pour apprendre à faire de la programmation multithread en Java 6 et 7, vous avez vraiment besoin de quelques blocs de construction. Nous allons commencer par ceux-ci:

  • Un programme fileté simple
  • Le filetage est une question de vitesse, non?
  • Défis de la concurrence Java
  • Quand utiliser Runnable
  • Quand les bons fils tournent mal
  • Nouveautés de Java 6 et 7
  • Quelle est la prochaine étape pour les threads Java

Cet article est une enquête pour débutants sur les techniques de threading Java, comprenant des liens vers certains des articles d'introduction les plus fréquemment lus de JavaWorld sur la programmation multithread. Démarrez vos moteurs et suivez les liens ci-dessus si vous êtes prêt à commencer à vous familiariser avec les threads Java dès aujourd'hui.

Un programme fileté simple

Considérez la source Java suivante.

Liste 1. FirstThreadingExample

class FirstThreadingExample { public static void main (String [] args) { // The second argument is a delay between // successive outputs. The delay is // measured in milliseconds. "10", for // instance, means, "print a line every // hundredth of a second". ExampleThread mt = new ExampleThread("A", 31); ExampleThread mt2 = new ExampleThread("B", 25); ExampleThread mt3 = new ExampleThread("C", 10); mt.start(); mt2.start(); mt3.start(); } } class ExampleThread extends Thread { private int delay; public ExampleThread(String label, int d) { // Give this particular thread a // name: "thread 'LABEL'". super("thread '" + label + "'"); delay = d; } public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { try { System.out.format("Line #%d from %s\n", count, getName()); Thread.currentThread().sleep(delay); } catch (InterruptedException ie) { // This would be a surprise. } } } }

Maintenant, compilez et exécutez cette source comme vous le feriez pour n'importe quelle autre application de ligne de commande Java. Vous verrez une sortie qui ressemble à ceci:

Listing 2. Sortie d'un programme threadé

Line #1 from thread 'A' Line #1 from thread 'C' Line #1 from thread 'B' Line #2 from thread 'C' Line #3 from thread 'C' Line #2 from thread 'B' Line #4 from thread 'C' ... Line #17 from thread 'B' Line #14 from thread 'A' Line #18 from thread 'B' Line #15 from thread 'A' Line #19 from thread 'B' Line #16 from thread 'A' Line #17 from thread 'A' Line #18 from thread 'A' Line #19 from thread 'A'

Voilà - vous êtes un Threadprogrammeur Java !

Eh bien, d'accord, peut-être pas si vite. Aussi petit que soit le programme de la liste 1, il contient des subtilités qui méritent notre attention.

Threads et indétermination

Un cycle d'apprentissage typique avec programmation comprend quatre étapes: (1) Étudier un nouveau concept; (2) exécuter un exemple de programme; (3) comparer la production aux attentes; et (4) itérer jusqu'à ce que les deux correspondent. Notez, cependant, que j'ai dit précédemment que la sortie pour FirstThreadingExampleressemblerait à "quelque chose comme" le Listing 2. Donc, cela signifie que votre sortie pourrait être différente de la mienne, ligne par ligne. De quoi s'agit - il?

Dans les programmes Java les plus simples, il y a une garantie d'ordre d'exécution: la première ligne d' main()entrée sera exécutée en premier, puis la suivante, et ainsi de suite, avec un traçage approprié dans et hors des autres méthodes. Threadaffaiblit cette garantie.

Threading apporte une nouvelle puissance à la programmation Java; vous pouvez obtenir des résultats avec des threads dont vous ne pourriez pas vous passer. Mais ce pouvoir vient au prix de la détermination . Dans les programmes Java les plus simples, il existe une garantie d'ordre d'exécution: la première ligne d' main()entrée sera exécutée en premier, puis la suivante, et ainsi de suite, avec un traçage approprié dans et hors des autres méthodes. Threadaffaiblit cette garantie. Dans un programme multithread, " Line #17 from thread B" peut apparaître sur votre écran avant ou après " Line #14 from thread A," et l'ordre peut différer lors des exécutions successives du même programme, même sur le même ordinateur.

L'indétermination peut être inconnue, mais elle n'a pas besoin d'être dérangeante. L'ordre d'exécution au sein d' un thread reste prévisible, et il existe également des avantages associés à l'indétermination. Vous avez peut-être rencontré quelque chose de similaire lorsque vous travaillez avec des interfaces utilisateur graphiques (GUI). Les écouteurs d'événements dans Swing ou les gestionnaires d'événements en HTML sont des exemples.

Bien qu'une discussion complète sur la synchronisation des threads n'entre pas dans le cadre de cette introduction, il est facile d'expliquer les bases.

Par exemple, considérez la mécanique de la façon dont HTML spécifie ... onclick = "myFunction();" ...pour déterminer l'action qui se produira après que l'utilisateur clique. Ce cas familier d'indétermination illustre certains de ses avantages. Dans ce cas, myFunction()n'est pas exécuté à un moment défini par rapport aux autres éléments du code source, mais par rapport à l'action de l'utilisateur final . L'indétermination n'est donc pas seulement une faiblesse du système; c'est aussi un enrichissement du modèle d'exécution, qui donne au programmeur de nouvelles opportunités pour déterminer la séquence et la dépendance.

Délais d'exécution et sous-classement des threads

Vous pouvez en tirer des leçons FirstThreadingExampleen les expérimentant par vous-même. Essayez d'ajouter ou de supprimer des ExampleThreads - c'est-à-dire des invocations de constructeur comme ... new ExampleThread(label, delay);- et de bricoler avec les delays. L'idée de base est que le programme démarre trois Threads séparés , qui s'exécutent ensuite indépendamment jusqu'à la fin. Pour rendre leur exécution plus instructive, chacun retarde légèrement entre les lignes successives qu'il écrit en sortie; cela donne aux autres threads une chance d'écrire leur sortie.

Notez que la Threadprogrammation basée sur la base de données ne nécessite pas, en général, de gérer un InterruptedException. Celui montré dans FirstThreadingExamplea à voir avec sleep(), plutôt que d'être directement lié à Thread. La Threadsource la plus basée n'inclut pas de sleep(); le but sleep()ici est de modéliser, d'une manière simple, le comportement de méthodes de longue durée trouvées «dans la nature».

Une autre chose à noter dans le Listing 1 est qu'il Threads'agit d'une classe abstraite , conçue pour être sous-classée. Sa run()méthode par défaut ne fait rien, elle doit donc être remplacée dans la définition de la sous-classe pour accomplir quelque chose d'utile.

Tout est question de vitesse, non?

Vous pouvez donc maintenant voir un peu ce qui rend la programmation avec des threads complexe. Mais l'essentiel pour endurer toutes ces difficultés n'est pas de gagner en vitesse.

Les programmes multithreads ne se terminent généralement pas plus rapidement que les programmes à thread unique - en fait, ils peuvent être considérablement plus lents dans les cas pathologiques. La valeur ajoutée fondamentale des programmes multithreads est la réactivité . Lorsque plusieurs cœurs de traitement sont disponibles pour la JVM, ou lorsque le programme passe beaucoup de temps à attendre plusieurs ressources externes telles que des réponses réseau, le multithreading peut aider le programme à se terminer plus rapidement.

Pensez à une application GUI: si elle répond toujours aux points et aux clics des utilisateurs finaux tout en recherchant «en arrière-plan» une empreinte digitale correspondante ou en recalculant le calendrier du tournoi de tennis de l'année prochaine, elle a été conçue avec la concurrence à l'esprit. Une architecture d'application simultanée typique place la reconnaissance et la réponse aux actions de l'utilisateur dans un thread distinct du thread de calcul affecté pour gérer la grande charge back-end. (Voir «Thring Swing et thread de distribution d'événements» pour une illustration plus détaillée de ces principes.)

Dans votre propre programmation, vous êtes donc plus susceptible d'envisager d'utiliser Threads dans l'une de ces circonstances:

  1. An existing application has correct functionality but is unresponsive at times. These "blocks" often have to do with external resources outside your control: time-consuming database queries, complicated calculations, multimedia playback, or networked responses with uncontrollable latency.
  2. A computationally-intense application could make better use of multicore hosts. This might be the case for someone rendering complex graphics or simulating an involved scientific model.
  3. Thread naturally expresses the application's required programming model. Suppose, for instance, that you were modeling the behavior of rush-hour automobile drivers or bees in a hive. To implement each driver or bee as a Thread-related object might be convenient from a programming standpoint, apart from any considerations of speed or responsiveness.

Défis de la concurrence Java

Le programmeur expérimenté Ned Batchelder a récemment plaisanté

Certaines personnes, lorsqu'elles sont confrontées à un problème, pensent: "Je sais, j'utiliserai des fils", puis deux, elles ont des erpoblesms.

C'est drôle parce que cela modélise si bien le problème de la concurrence. Comme je l'ai déjà mentionné, les programmes multithreads sont susceptibles de donner des résultats différents en termes de séquence exacte ou de synchronisation d'exécution du thread. C'est troublant pour les programmeurs, qui sont formés à penser en termes de résultats reproductibles, de détermination stricte et de séquence invariante.

It gets worse. Different threads might not only produce results in different orders, but they can contend at more essential levels for results. It's easy for a newcomer to multithreading to close() a file handle in one Thread before a different Thread has finished everything it needs to write.

Testing concurrent programs

Ten years ago on JavaWorld, Dave Dyer noted that the Java language had one feature so "pervasively used incorrectly" that he ranked it as a serious design flaw. That feature was multithreading.

Dyer's comment highlights the challenge of testing multithreaded programs. When you can no longer easily specify the output of a program in terms of a definite sequence of characters, there will be an impact on how effectively you can test your threaded code.

The correct starting point to resolving the intrinsic difficulties of concurrent programming was well stated by Heinz Kabutz in his Java Specialist newsletter: recognize that concurrency is a topic that you should understand and study it systematically. There are of course tools such as diagramming techniques and formal languages that will help. But the first step is to sharpen your intuition by practicing with simple programs like FirstThreadingExample in Listing 1. Next, learn as much as you can about threading fundamentals like these:

  • Synchronization and immutable objects
  • Thread scheduling and wait/notify
  • Race conditions and deadlock
  • Thread monitors for exclusive access, conditions, and assertions
  • JUnit best practices -- testing multithreaded code

When to use Runnable

Object orientation in Java defines singly inherited classes, which has consequences for multithreading coding. To this point, I have only described a use for Thread that was based on subclasses with an overridden run(). In an object design that already involved inheritance, this simply wouldn't work. You cannot simultaneously inherit from RenderedObject or ProductionLine or MessageQueue alongside Thread!

This constraint affects many areas of Java, not just multithreading. Fortunately, there's a classical solution for the problem, in the form of the Runnable interface. As explained by Jeff Friesen in his 2002 introduction to threading, the Runnable interface is made for situations where subclassing Thread isn't possible:

L' Runnableinterface de déclare une signature de méthode unique: void run();. Cette signature est identique à Threadla run()signature de méthode de et sert d'entrée d'exécution du thread. Comme il Runnables'agit d'une interface, toute classe peut implémenter cette interface en attachant une implementsclause à l'en-tête de classe et en fournissant une run()méthode appropriée . Au moment de l'exécution, le code du programme peut créer un objet, ou exécutable , à partir de cette classe et passer la référence de l'exécutable à un Threadconstructeur approprié .

Donc, pour les classes qui ne peuvent pas s'étendre Thread, vous devez créer un exécutable pour tirer parti du multithreading. Sémantiquement, si vous faites de la programmation au niveau du système et que votre classe est dans une relation is-a to Thread, vous devez sous-classer directement à partir de Thread. Mais la plupart des utilisations du multithreading au niveau de l'application reposent sur la composition, et définissent ainsi une Runnablecompatibilité avec le diagramme de classes de l'application. Heureusement, il suffit d'une ou deux lignes supplémentaires pour coder à l'aide de l' Runnableinterface, comme indiqué dans la liste 3 ci-dessous.