Java 101: la concurrence Java sans la douleur, partie 1

Avec la complexité croissante des applications simultanées, de nombreux développeurs trouvent que les capacités de thread de bas niveau de Java sont insuffisantes pour leurs besoins de programmation. Dans ce cas, il est peut-être temps de découvrir les utilitaires de concurrence Java. Commencez avec java.util.concurrent, avec l'introduction détaillée de Jeff Friesen au framework Executor, aux types de synchroniseurs et au package Java Concurrent Collections.

Java 101: la nouvelle génération

Le premier article de cette nouvelle série JavaWorld présente l' API Java Date and Time .

La plate-forme Java fournit des capacités de thread de bas niveau qui permettent aux développeurs d'écrire des applications simultanées où différents threads s'exécutent simultanément. Cependant, le thread Java standard présente certains inconvénients:

  • Java primitives de bas de concurrence au niveau ( synchronized, volatile, wait(), notify()et notifyAll()) ne sont pas faciles à utiliser correctement. Les risques de threads tels que le blocage, la famine de thread et les conditions de concurrence, qui résultent d'une utilisation incorrecte des primitives, sont également difficiles à détecter et à déboguer.
  • S'appuyer sur la synchronizedcoordination de l'accès entre les threads entraîne des problèmes de performances qui affectent l'évolutivité des applications, une exigence pour de nombreuses applications modernes.
  • Les capacités de thread de base de Java sont de niveau trop bas. Les développeurs ont souvent besoin de constructions de plus haut niveau comme des sémaphores et des pools de threads, que les capacités de threading de bas niveau de Java n'offrent pas. En conséquence, les développeurs créeront leurs propres constructions, ce qui est à la fois chronophage et sujet aux erreurs.

Le framework JSR 166: Concurrency Utilities a été conçu pour répondre au besoin d'une fonction de threading de haut niveau. Lancé au début de 2002, le framework a été formalisé et implémenté deux ans plus tard dans Java 5. Des améliorations ont suivi dans Java 6, Java 7 et le prochain Java 8.

Cette série en deux parties Java 101: La prochaine génération présente aux développeurs de logiciels familiers avec les threads Java de base les packages et le cadre des utilitaires Java Concurrency. Dans la partie 1, je présente un aperçu du framework Java Concurrency Utilities et présente son framework Executor, les utilitaires de synchronisation et le package Java Concurrent Collections.

Comprendre les threads Java

Avant de vous lancer dans cette série, assurez-vous de bien connaître les bases du filetage. Commencez par l' introduction de Java 101 aux capacités de thread de bas niveau de Java:

  • Partie 1: Présentation des threads et des exécutables
  • Partie 2: Synchronisation des threads
  • Partie 3: Planification des threads, attente / notification et interruption des threads
  • Partie 4: Groupes de threads, volatilité, variables locales de thread, minuteries et mort de thread

Dans les utilitaires de concurrence Java

Le framework Java Concurrency Utilities est une bibliothèque de types conçus pour être utilisés comme blocs de construction pour la création de classes ou d'applications simultanées. Ces types sont thread-safe, ont été minutieusement testés et offrent des performances élevées.

Les types dans les utilitaires de concurrence Java sont organisés en petits cadres; à savoir, le framework Executor, le synchroniseur, les collections simultanées, les verrous, les variables atomiques et Fork / Join. Ils sont ensuite organisés en un package principal et une paire de sous-packages:

  • java.util.concurrent contient des types d'utilitaires de haut niveau couramment utilisés dans la programmation simultanée. Les exemples incluent les sémaphores, les barrières, les pools de threads et les hashmaps simultanés.
    • Le sous- package java.util.concurrent.atomic contient des classes d'utilitaires de bas niveau qui prennent en charge la programmation thread-safe sans verrouillage sur des variables uniques.
    • Le sous- package java.util.concurrent.locks contient des types d'utilitaires de bas niveau pour le verrouillage et l'attente de conditions, qui diffèrent de l'utilisation de la synchronisation de bas niveau et des moniteurs de Java.

Le framework Java Concurrency Utilities expose également l' instruction matérielle de bas niveau de comparaison et d'échange (CAS) , dont des variantes sont généralement prises en charge par les processeurs modernes. CAS est beaucoup plus léger que le mécanisme de synchronisation basé sur le moniteur de Java et est utilisé pour implémenter certaines classes simultanées hautement évolutives. La java.util.concurrent.locks.ReentrantLockclasse basée sur CAS , par exemple, est plus performante que la synchronizedprimitive équivalente basée sur un moniteur . ReentrantLockoffre plus de contrôle sur le verrouillage. (Dans la partie 2, j'expliquerai plus en détail comment CAS fonctionne java.util.concurrent.)

System.nanoTime ()

Le framework Java Concurrency Utilities comprend long nanoTime(), qui est membre de la java.lang.Systemclasse. Cette méthode permet d'accéder à une source de temps de granularité nanoseconde pour effectuer des mesures de temps relatif.

Dans les sections suivantes, je présenterai trois fonctionnalités utiles des utilitaires de concurrence Java, en expliquant d'abord pourquoi ils sont si importants pour la concurrence moderne, puis en démontrant comment ils fonctionnent pour augmenter la vitesse, la fiabilité, l'efficacité et l'évolutivité des applications Java simultanées.

Le cadre Executor

Dans le threading, une tâche est une unité de travail. Un problème avec les threads de bas niveau en Java est que la soumission de tâches est étroitement associée à une politique d'exécution de tâches, comme le montre le Listing 1.

Liste 1. Server.java (Version 1)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; class Server { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; new Thread(r).start(); } } static void doWork(Socket s) { } }

Le code ci-dessus décrit une application serveur simple (à doWork(Socket)gauche par souci de concision). Le thread serveur appelle socket.accept()à plusieurs reprises pour attendre une demande entrante, puis démarre un thread pour traiter cette demande à son arrivée.

Étant donné que cette application crée un nouveau thread pour chaque demande, elle ne s'adapte pas bien lorsqu'elle est confrontée à un grand nombre de demandes. Par exemple, chaque thread créé nécessite de la mémoire et trop de threads peuvent épuiser la mémoire disponible, forçant l'application à se terminer.

Vous pouvez résoudre ce problème en modifiant la stratégie d'exécution des tâches. Plutôt que de toujours créer un nouveau thread, vous pouvez utiliser un pool de threads, dans lequel un nombre fixe de threads traiterait les tâches entrantes. Vous devrez toutefois réécrire l'application pour effectuer cette modification.

java.util.concurrentinclut le cadre Executor, un petit cadre de types qui découplent la soumission des tâches des politiques d'exécution des tâches. En utilisant le framework Executor, il est possible d'ajuster facilement la politique d'exécution des tâches d'un programme sans avoir à réécrire de manière significative votre code.

À l'intérieur du cadre Executor

Le framework Executor est basé sur l' Executorinterface, qui décrit un exécuteur comme tout objet capable d'exécuter des java.lang.Runnabletâches. Cette interface déclare la méthode solitaire suivante pour exécuter une Runnabletâche:

void execute(Runnable command)

Vous soumettez une Runnabletâche en la transmettant à execute(Runnable). Si l'exécuteur ne peut pas exécuter la tâche pour une raison quelconque (par exemple, si l'exécuteur a été arrêté), cette méthode lèvera un RejectedExecutionException.

Le concept clé est que la soumission des tâches est découplée de la politique d'exécution des tâches , qui est décrite par une Executorimplémentation. La tâche exécutable est ainsi capable de s'exécuter via un nouveau thread, un thread poolé, le thread appelant, etc.

Notez que Executorc'est très limité. Par exemple, vous ne pouvez pas arrêter un exécuteur ou déterminer si une tâche asynchrone est terminée. Vous ne pouvez pas non plus annuler une tâche en cours d'exécution. Pour ces raisons et d'autres, le framework Executor fournit une interface ExecutorService, qui s'étend Executor.

Cinq ExecutorServiceméthodes sont particulièrement remarquables:

  • boolean awaitTermination (long timeout, unité TimeUnit) bloque le thread appelant jusqu'à ce que toutes les tâches aient terminé leur exécution après une demande d'arrêt, que le délai expire ou que le thread actuel soit interrompu, selon la première éventualité. Le temps d'attente maximal est spécifié par timeout, et cette valeur est exprimée dans les unitunités spécifiées par l' TimeUniténumération; par exemple TimeUnit.SECONDS,. Cette méthode se lance java.lang.InterruptedExceptionlorsque le thread actuel est interrompu. Il renvoie true lorsque l'exécuteur est terminé et false lorsque le délai d'attente s'écoule avant la fin.
  • boolean isShutdown () retourne true lorsque l'exécuteur a été arrêté.
  • void shutdown () lance un arrêt ordonné dans lequel les tâches précédemment soumises sont exécutées mais aucune nouvelle tâche n'est acceptée.
  • Future submit (tâche appelable) soumet une tâche de retour de valeur pour exécution et renvoie un Futurereprésentant les résultats en attente de la tâche.
  • La soumission future (tâche exécutable) soumet une Runnabletâche pour exécution et renvoie une Futurereprésentation de cette tâche.

L' Futureinterface représente le résultat d'un calcul asynchrone. Le résultat est connu comme un futur car il ne sera généralement disponible qu'à un moment donné dans le futur. Vous pouvez appeler des méthodes pour annuler une tâche, renvoyer le résultat d'une tâche (attente indéfinie ou pour un délai d'attente lorsque la tâche n'est pas terminée) et déterminer si une tâche a été annulée ou s'est terminée.

L' Callableinterface est similaire à l' Runnableinterface en ce qu'elle fournit une méthode unique décrivant une tâche à exécuter. Contrairement à Runnablela void run()méthode de, Callablela V call() throws Exceptionméthode de peut retourner une valeur et lancer une exception.

Méthodes d'usine de l'exécuteur

At some point, you'll want to obtain an executor. The Executor framework supplies the Executors utility class for this purpose. Executors offers several factory methods for obtaining different kinds of executors that offer specific thread-execution policies. Here are three examples:

  • ExecutorService newCachedThreadPool() creates a thread pool that creates new threads as needed, but which reuses previously constructed threads when they're available. Threads that haven't been used for 60 seconds are terminated and removed from the cache. This thread pool typically improves the performance of programs that execute many short-lived asynchronous tasks.
  • ExecutorService newSingleThreadExecutor() creates an executor that uses a single worker thread operating off an unbounded queue -- tasks are added to the queue and execute sequentially (no more than one task is active at any one time). If this thread terminates through failure during execution before shutdown of the executor, a new thread will be created to take its place when subsequent tasks need to be executed.
  • ExecutorService newFixedThreadPool(int nThreads) creates a thread pool that re-uses a fixed number of threads operating off a shared unbounded queue. At most nThreads threads are actively processing tasks. If additional tasks are submitted when all threads are active, they wait in the queue until a thread is available. If any thread terminates through failure during execution before shutdown, a new thread will be created to take its place when subsequent tasks need to be executed. The pool's threads exist until the executor is shut down.

The Executor framework offers additional types (such as the ScheduledExecutorService interface), but the types you are likely to work with most often are ExecutorService, Future, Callable, and Executors.

See the java.util.concurrent Javadoc to explore additional types.

Travailler avec le framework Executor

Vous constaterez que le framework Executor est assez facile à utiliser. Dans la liste 2, j'ai utilisé Executoret Executorspour remplacer l'exemple de serveur de la liste 1 par une alternative plus évolutive basée sur un pool de threads.

Listing 2. Server.java (Version 2)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executor; import java.util.concurrent.Executors; class Server { static Executor pool = Executors.newFixedThreadPool(5); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; pool.execute(r); } } static void doWork(Socket s) { } }

Le listing 2 utilise newFixedThreadPool(int)pour obtenir un exécuteur basé sur un pool de threads qui réutilise cinq threads. Il remplace également new Thread(r).start();par pool.execute(r);pour exécuter des tâches exécutables via l'un de ces threads.

Le listing 3 présente un autre exemple dans lequel une application lit le contenu d'une page Web arbitraire. Il affiche les lignes résultantes ou un message d'erreur si le contenu n'est pas disponible dans un délai maximum de cinq secondes.