Le framework Executor

Formation

À Paris

Prix sur demande

Appeler le centre

Avez-vous besoin d'un coach de formation?

Il vous aidera à comparer différents cours et à trouver la solution la plus abordable.

Description

  • Typologie

    Formation

  • Lieu

    Paris

Nous vous proposons des cours ouverts pour se former autrement, pour devenir acteur de votre vie. Nous vous aidons à prendre votre envol, mais ça ne s'arrête pas là. Notre volonté est de vous accompagner tout au long de votre vie, dans votre parcours professionnel.Grâce à cette formation vous pourrez acquérir les connaissances nécessaires qui vous permettrons d’ajouter des compétences à votre profil et obtenir de solides aptitude qui vous offriront de nombreuses opportunités professionnelles.

Les sites et dates disponibles

Lieu

Date de début

Paris ((75) Paris)
Voir plan
7 Cité Paradis, 75010

Date de début

Consulter

Questions / Réponses

Ajoutez votre question

Nos conseillers et autres utilisateurs pourront vous répondre

À qui souhaitez-vous addresser votre question?

Saisissez vos coordonnées pour recevoir une réponse

Nous ne publierons que votre nom et votre question

Les Avis

Le programme

Introduction du cours

Bonjour à tous,

Tout d'abord, sachez que la programmation concurrente est un sujet très vaste et souvent complexe, rempli de pièges et de comportements tous plus étranges les uns que les autres .

Dans son big-tuto, Cysboy a introduit les threads et a illustré leur complexité. Je ne vais pas aborder ici les moyens d'assurer la thread-safety (le fait qu'un objet se comporte correctement quand plusieurs threads y accèdent), cela fera peut-être l'objet d'un futur tutoriel. Pour en revenir aux threads, JAVA 5 a introduit un nouveau moyen d'exécuter les tâches en parallèles : le framework Executor.

Je ne vais pas vous l'expliquer en intégralité, mais vous devriez être à même d'en comprendre les grandes lignes.

Je pars du principe que vous avez déjà une certaine pratique de Java et que vous avez déjà entendu le mot thread. Si les collections, les génériques et la programmation Java en général vous sont complètement étrangers, commencez par le tutoriel de Cysboy.

Sans plus attendre amis zéros, voyons ce que nous réserve le framework Executor.

Un petit rappelL'utilisation des threads

Les threads sont omniprésents dans les programmes java. Votre JVM (Java Virtual Machine), qui se charge d'exécuter votre programme, va créer des threads à votre insu : les threads qui s'occupent des opérations de nettoyage de la mémoire et le thread d'exécution principal de votre programme.
Je profite également de ce court rappel sur les threads pour préciser certaines choses :

  • L'ordre d'exécution des threads n'est pas garanti. J'entends par là que, à code équivalent, l'ordre d'éxecution des threads peut changer.

  • Je vois souvent les débutants se poser des questions quand à l'utilité de l'encapsulation ou l'utilisation du mot-clé final dans le code. Je ne vous dirai que ceci : l'encapsulation et les objets immuables sont vos meilleurs alliés pour vous en sortir dans un environnement multi-threads en java (ceci étant un tuto sur java je n'élargirais pas le débat à d'autres langages). Je vous précise au passage que l'encapsulation n'a que très rarement un impact sur les performances mais c'est un autre débat.

Je ne doute pas une seule seconde que les threads n'ont absolument aucun secret pour vous mais juste pour vous rafraichir la mémoire, généralement on créé un thread de cette façon :

On implémente l'interface java.lang.Runnable

public class MonRunnable implements Runnable { @Override public void run(){ System.out.println("Travail à effectuer"); } }

et on l'enveloppe dans un thread :

public class Main { public static void main(String[] args){ //On créer notre thread avec notre tache à executer Thread t = new Thread(new MonRunnable()); //On lance le thread t.start(); } }

Je sais, vous le saviez déjà.

Les basesUn peu d'histoire

Nous sommes en 2004, Java 5 (Tiger) viens de sortir. Comme toutes les versions, celle-ci apporte sont lots de nouveautés dont l'auto-boxing, auto-unboxing, les génériques et la nouvelle API de concurrence. Désignée sous le doux nom de JSR 166 (http://www.jcp.org/en/jsr/detail?id=166) et conçu sous la direction (il n'était pas le seul bien sur) de Doug Lea, enseignant à l'université d'état d'Oswego aux Etats-Unis, cette JSR introduit, entre autres choses, un nouveau framework d'éxecution des tâches connu sous le nom de Framework Executor.
6 ans plus tard, Executor semble peu connu voir totalement ignoré dans le programme des universités.

L'idée d'Executor et des autres outils présents dans le package java.util.concurrent, est de pouvoir choisir facilement la politique d'exécution de nos tâches.

Qu'est-ce qu'une politique d'exécution ? Et bien c'est relativement simple. La politique d'exécution répond globalement à 3 questions :

  • Quelles tâches vont être exécutées ?

  • Dans quel ordre ?

  • Combien de ses tâches peut on exécuter en parallèle ?

Avant Java 5, c'était aux développeurs d’implémenter les classes nécessaire à la politique qu'ils souhaitaient mettre en place (Files d'exécutions, pool de threads...).

Ceci étant dit, explorons Executor.

Les base du framework Executor

Autant le dire tout de suite : vous ne manipulez plus les threads directement !

L'abstraction de base que vous allez utiliser est l'interface Executor.

public interface Executor { void execute(Runnable command); }

J'aimerais ici faire un petit aparté sur les interfaces. Comme l'a très bien expliqué Cysboy dans son tutoriel les interfaces permettent de créer des super-types et donc de permettre un polymorphisme. De ces deux choses découlent le principal intérêt des interfaces, le découplage.

Je m'explique :
Une interface A décrit un certain nombres d'opérations

public interface A { void methodeA(); void methodeB(); }

On veut, quelle que soit la classe qui implémente A, faire un traitement particulier. Vous pouvez sans problème écrire une méthode qui prend en paramètre une interface, car vous savez quelles méthodes seront implémentées dans les classes qui implémentent votre interface. Si la classe qui implémente l'interface A change (pour une autre classe d'implémentation plus adaptée par exemple), la méthode execute (voir exemple ci-après) n'aura pas besoin d'être changée. C'est la raison pour laquelle on utilise des interfaces.

public class Execution { public void execute(A a) { a.methodeA(); a.methodeB(); } }

Créer un framework basé sur les interfaces permet donc une grande souplesse dans l'exécution des tâches. Vous pourrez exécuter n'importe quelle tâche qui implémentent Runnable.

Comme dit précédemment, les executors exécutent tout comme les threads, des Runnables. Nous allons reprendre l'exemple précédent en utilisant Executor cette fois :

Au lieu de créer un thread avec new Thread(...), nous allons utiliser Executor qui fournit une méthode pour produire le même effet : la méthode newSingleThreadExecutor().

public class Main { public static void main(String[] args) { Executor executor = Executors.newSingleThreadExecutor(); executor.execute(new MonRunnable()); } }

Il y a 3 interfaces à connaitre :

  • Executor : Pour l'exécution des "Runnables"

  • ExecutorService : Pour l'exécution des tâches "Runnables" et "Callables"(les Callables seront expliqués par la suite)

  • ScheduledExecutorService : Pour l'exécution des tâches périodiques (qui se répète dans le temps) et différée (la tâche doit commencer dans 60 secondes par exemple)

La relation entre ces interfaces est la suivante :

Pour les zéros un peu allergique à l'UML :-° : ScheduledExecutorService hérite de ExecutorService, et ExecutorService hérite de Executor.

Cependant vous le savez, on n'instancie pas une interface. Pour créer les bons objets nous allons nous en remettre à une seule classe : Executors. Notez bien Executors; C'est une classe qui contient toutes les méthodes statiques nécessaire à la création d'objets du framework Executor.

Dans l'exemple précédent j'ai utilisé cette classe :

public class Main { public static void main(String[] args) { Executor executor = Executors.newSingleThreadExecutor(); executor.execute(new MonRunnable()); } }

La méthode newSingleThreadExecutor(); renvoie un executor mono-thread. Je vous rappelle que vous ne manipulez pas les threads directement.

Executor contient plusieurs fabriques qui ne renvoient que des executors mono-thread :

  • Executors.newSingleThreadExecutor() : Executor mono-thread classique

  • Executors.newSingleThreadScheduledExecutor() : Executor mono-thread pour les tâches périodiques

Nous verrons les autres méthodes par la suite.

Exemple

Précedemment je vous ai montré le newSingleThreadExecutor() donc je vais vous montrer le newSingleThreadScheduledExecutor().

Pour information pour faire des tâches périodiques avec des threads on utilisait les "TimerTask" et la classe utilitaire "Timer". Cependant Timer ne s'appuyait que sur le temps absolu (de votre horloge système) qui risquait d'être changée, alors que Executor ne s’appuie que sur le temps relatif.

Ceci étant dit, sans plus attendre voici un exemple de tâche exécuté toutes les secondes.

public class Main { public static void main(String[] args){ ScheduledExecutorService execute = Executors.newSingleThreadScheduledExecutor(); //Execute MonRunnable toutes les secondes execute.scheduleAtFixedRate(new MonRunnable(), 0, 1, TimeUnit.SECONDS); } }

L’énumération TimeUnit apporte une plus grande clarté au code. Cela évite d'avoir à écrire le temps en milli-secondes, ce qui est illisible arrivé à un certain point.

Simple non ?

Les pools de threads

Bon les exécutions mono-thread c'est bien gentil mais je ne veux pas attendre qu'une tâche se termine avant de faire la suivante moi ! Il est où le parallélisme sinon ...

Merci de poser la question

Comme vous avez sans doute pu le voir si vous développez dans un IDE (hé oui dans le bloc note il n'y a pas d'auto-complétion), la classe Executors possède des méthodes aux noms un peu étrange : newFixedThreadPool(), newScheduledThreadPool(), newCachedThreadPool()...

Si vous ne savez pas ce qu'est un pool d'objets, et bien pour simplifier disons que c'est une collection d'objets créés et initialisés puis mis en mémoire. On les utilise quand le coût de création et d'initialisation d'un objet est assez important. Le fait de mettre certains objets en mémoire peut augmenter les performances dans certain cas mais je ne vais pas rentrer dans les détails de la performance en java.

Les tailles des pools de threads ne sont pratiquement jamais codées en dur. La taille des pools dépend en effet des ressources. Passer de 3 à 100 processeurs n'est pas négligeable et le programme doit prendre en compte ces changements.

Il existe une méthode pour récupérer dynamiquement le nombre de processeurs disponibles:

int proc = Runtime.getRuntime().availableProcessors();

Bref, revenons à notre Executors.

Le but ici est de ne pas utiliser le même thread pour toutes les tâches mais d'utiliser un thread différent pour chacune. Donc il n'y a plus besoin d'attendre qu'une tâche soit finie pour exécuter la suivante.
Allons-y pas à pas, nous allons commencer par modifier la classe MonRunnable pour observer dans quel thread on se trouve.

La nouvelle classe Runnable :

public class MonRunnable implements Runnable { @Override public void run() { try { //On simule un traitement long en mettant en pause le Thread pendant 4 secondes Thread.sleep(4000); //On affiche le nom du thread où on se trouve System.out.println("Execution dans le thread " + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } }

Nous allons tout d'abord l'exécuter en mono-thread, c'est à dire un thread pour toutes nos tâches.

public class Main { public static void main(String[] args) { //La liste qui va stocker les taches à effectuer List<Runnable> runnables = new ArrayList<Runnable>(); //On créer 4 taches (instance de MonRunnable) runnables.add(new MonRunnable()); runnables.add(new MonRunnable()); runnables.add(new MonRunnable()); runnables.add(new MonRunnable()); //Notre executor mono-thread ExecutorService execute = Executors.newSingleThreadExecutor(); //La méthode qui se charge de l'exécution des tâches executeRunnables(execute, runnables); } public static void executeRunnables(final ExecutorService service, List<Runnable> runnables){ for(Runnable r : runnables){ service.execute(r); } //On ferme l'executor une fois les taches finies //En effet shutdown va attendre la fin d'exécution des tâches service.shutdown(); } }

Résultat à l'exécution:

Modifions le code pour utiliser un pool de threads : chaque thread est lancé en parallèle et exécute la tâche qui lui est dédiée. (j'ai commenté le code pour que vous compreniez).

public class Main { public static void main(String[] args){ List<Runnable> runnables = new ArrayList<Runnable>(); runnables.add(new MonRunnable()); runnables.add(new MonRunnable()); runnables.add(new MonRunnable()); runnables.add(new MonRunnable()); //Cette fois on créer un pool de 10 threads maximum ExecutorService execute = Executors.newFixedThreadPool(10); executeRunnables(execute, runnables); } public static void executeRunnables(final ExecutorService service, List<Runnable> runnables){ //On exécute chaque "Runnable" de la liste "runnables" for(Runnable r : runnables){ service.execute(r); } service.shutdown(); } }

Résultat à l'exécution :

Executor facilite grandement l'exécution de tâches en parallèles.

Si tous les threads du pool sont occupés les tâches sont placées dans une file d'attente jusqu'à ce qu'un thread soit libre.

Un exemple un peu plus parlant

Les pools de threads sont généralement utilisés pour exécuter des tâches homogènes et indépendantes. Les serveurs qui traitent des requêtes sont un excellent exemple d'utilisation. Les clients qui se connectent au serveur sont indépendants et le traitement est toujours le même.

Je vous renvoie au cours de SoftDeath "Introduction aux sockets", pour bien comprendre cet exemple.

Nous allons donc mettre en place un simple serveur multi-threads qui va traiter les connexions entrantes dans un thread différent. Mais au lieu de créer un thread manuellement à chaque connexion et donc gérer son cycle de vie manuellement, nous allons utiliser un pool de threads qui va faire une taille bien définie pour être sur de contrôler le nombre de threads créés.

public class ServerLauncher { public static void main(String[] args){ //On se sert d'un pool de thread pour limiter le nombre de threads //en mémoire final ExecutorService service =...

Appeler le centre

Avez-vous besoin d'un coach de formation?

Il vous aidera à comparer différents cours et à trouver la solution la plus abordable.

Le framework Executor

Prix sur demande