Académique Documents
Professionnel Documents
Culture Documents
Aujourd'hui, le moindre quipement lectronique - ordinateur, tlphone, tablette... possde plusieurs curs, rpartis sur un ou plusieurs processeurs. Si l'on souhaite en tirer le meilleur parti, il est ncessaire de se pencher sur les arcanes de la programmation concurrente. Dans cet article, nous verrons ce que sont les threads, et comment les crer et les manipuler en Java. Cet article est issu d'un billet du blog de Zenika qui a autoris Developpez.com mettre disposition pour ses membres. Pour ragir au contenu de cet article, un espace de dialogue vous est propos sur le forum .
I - Introduction..............................................................................................................................................................3 II - La classe java.lang.Thread.................................................................................................................................... 3 II-A - Crer et dmarrer un thread.........................................................................................................................3 II-B - Nommer les threads..................................................................................................................................... 3 II-C - Start vs Run..................................................................................................................................................4 II-D - Vie et mort d'un thread.................................................................................................................................4 III - Runnable............................................................................................................................................................... 4 IV - Le framework Executor.........................................................................................................................................5 IV-A - Executor et ExecutorService....................................................................................................................... 5 IV-B - Callable et Future........................................................................................................................................ 7 IV-B-1 - Callable................................................................................................................................................7 IV-B-2 - Future.................................................................................................................................................. 8 IV-C - ExecutorService en action...........................................................................................................................9 IV-D - CompletionService.....................................................................................................................................11 IV-E - InvokeAny : que le meilleur gagne !.......................................................................................................... 12 V - Conclusion et remerciements.............................................................................................................................. 12
-2Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
I - Introduction
Un processus reprsente l'environnement d'excution d'un programme. Il rfrence d'une part un espace mmoire permettant de stocker les donnes propres l'application, et d'autre part un ensemble de threads permettant l'excution du code qui manipulera ces donnes. En Java, au dmarrage de l'application, un thread initial est cr : le thread "main". Son rle est de localiser le point d'entre de l'application (la mthode public static void main(String... args)) puis d'excuter son code. Ce thread, comme tous les threads, excute la squence d'instructions qui lui est confie de manire purement squentielle. Si une instruction prend du temps complter (par exemple, en attente de connexion un serveur), toute l'application est paralyse. Pour viter cela, il est possible (et mme souhaitable) de confier l'excution de ces portions bloquantes des threads annexes, laissant ainsi le thread principal libre de continuer l'excution de l'application. Voyons comment.
#1
On voit ici que la ligne #2 a t excute dans le thread principal de l'application, alors que la ligne #1 a t excute dans un autre thread ("Thread-0").
-3Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
Ce nom lui a t attribu automatiquement par la JVM, mais n'est pas trs parlant. Pour faciliter le dbogage, il est recommand de donner des noms explicites nos threads, en les passant au constructeur :
Thread t = new Thread("Mon thread") { ... };
Nous constatons que les deux instructions println() ont t excutes dans le mme thread :
Je suis dans le thread : main Je suis dans le thread : main
Pensez donc bien appeler start() et non pas run() lorsque vous souhaitez dmarrer un thread !
III - Runnable
L'utilisation que nous faisons de la classe java.lang.Thread ci-dessus a le dfaut de lier fortement la dfinition du traitement excuter (le "quoi") au thread particulier qui l'excute (le "comment"). Que faire si l'on souhaite faire excuter le mme traitement par plusieurs threads ? Ou relancer un traitement aprs la mort du thread associ ? Nous comprenons ici la ncessit de dcoupler le "quoi" du "comment".
-4Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
C'est ici qu'intervient l'interface java.lang.Runnable, qui permet d'encapsuler un traitement sous la forme d'un composant autonome et rutilisable. Pour tre rellement excut, un Runnable doit tre pass en paramtre un Thread ou un ExecutorService (voir plus loin). Dans l'exemple suivant, un mme Runnable est pass deux threads :
public class NewThreadWithRunnable { public static void main(String... args) { // Notre traitement, encapsul dans un Runnable Runnable job = new Runnable() { // #1 public void run() { System.out.println("Je suis dans le thread : " + Thread.currentThread().getName()); } }; Thread t1 = new Thread(job, "Premier thread"); //#2 t1.start(); Thread t2 = new Thread(job, "Second thread"); t2.start(); System.out.println("Je suis dans le thread : " + Thread.currentThread().getName()); } }
En #2 nous passons le traitement excuter, encapsul dans un Runnable en #1, deux threads.
Je suis dans le thread : Premier thread Je suis dans le thread : main Je suis dans le thread : Second thread
Notez que l'ordre des lignes peut varier chez vous, car l'ordre dans lequel le processeur choisit d'excuter les diffrents threads est imprvisible.
IV - Le framework Executor
Jusqu'ici, nous avons cr et lanc manuellement un nouveau Thread chaque fois que nous souhaitions excuter un traitement de manire asynchrone. Si cette technique fonctionne bien pour les petites applications, elle est fortement dconseille pour les applications d'entreprise : d'une part, chaque thread supplmentaire augmente la mmoire consomme, la complexit globale de l'application, et le risque de contention ; d'autre part, pour de petites tches, le cot de cration d'un nouveau thread peut se rvler suprieur au cot d'excution du traitement associ.
Introduit avec Java 5, le framework Executor rpond ces problmatiques. Disponible dans le package java.util.concurrent, il fournit un pool de threads robuste et hautement configurable, ainsi que les classes Callable et Future qui tendent les fonctionnalits des Runnables (voir plus loin). Sa mise en uvre doit systmatiquement tre prfre la cration manuelle de threads.
-5Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
Je vous sens un peu dus. Effectivement, c'est assez pauvre. En ralit, toute la puissance du framework est exprime dans une autre interface : ExecutorService, qui drive d'Executor. Elle fournit des mthodes pour soumettre des traitements excuter (individuels ou en masse), ainsi que pour grer le cycle de vie du pool :
public interface ExecutorService extends Executor { // Job submission public void submit(Runnable job); public Future<V> submit(Callable<V> job); // Lifecycle management public void shutdown(); public void shutdownNow(); public boolean isShutdown(); // (...) }
La classe ThreadPoolExecutor, qui implmente cette interface, est tellement configurable que ses auteurs ont prfr fournir une factory couvrant les besoins les plus courants :
public class Executors { public ExecutorService public ExecutorService public ExecutorService public ExecutorService public ExecutorService // (...) } newSingleThreadExecutor() newFixedThreadPool(int nbThreads) newCachedThreadPool() newSingleThreadScheduledExecutor() newScheduledThreadPool(int nbThreads) {...}; {...}; {...}; {...}; {...};
// Pool avec 4 threads ExecutorService pool = Executors.newFixedThreadPool(4); pool.submit(job); pool.submit(job); pool.shutdown(); System.out.println("Je suis dans le thread : " + Thread.currentThread().getName()); } }
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
Afin de prouver que le pool recycle les threads au lieu d'en recrer, tentons de soumettre notre Runnable dix fois :
Je Je Je Je Je Je Je Je Je Je Je suis suis suis suis suis suis suis suis suis suis suis dans dans dans dans dans dans dans dans dans dans dans le le le le le le le le le le le thread thread thread thread thread thread thread thread thread thread thread : : : : : : : : : : : pool-1-thread-1 pool-1-thread-4 pool-1-thread-3 pool-1-thread-2 pool-1-thread-2 pool-1-thread-2 pool-1-thread-2 pool-1-thread-3 pool-1-thread-4 pool-1-thread-1 main
Nous voyons ici que le thread "pool-1-thread-1" a t sollicit deux fois, le thread "pool-1-thread-2" quatre fois, etc.
Un Callable est prvu pour tre soumis la mthode submit() d'un pool de threads, qui excutera son traitement de manire asynchrone.
public class NewExecutorServiceWithCallable { public static void main(String[] args) { Callable<Void> job = new Callable<Void>() { public Void call() throws Exception{ System.out.println("Je suis dans le thread " + Thread.currentThread().getName()); return null; } }; ExecutorService pool = Executors.newFixedThreadPool(4); pool.submit(job); pool.shutdown(); System.out.println("Je suis dans le thread " + Thread.currentThread().getName()); } }
-7Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
IV-B-2 - Future
Une fois soumis un pool de threads, un Callable est gnralement[1] excut de manire asynchrone ; le rsultat produit ne sera sans doute pas disponible avant un certain temps. Du point de vue de l'appelant (celui qui soumet le traitement au pool), cela n'aurait aucun sens d'attendre ce rsultat de manire synchrone : il perdrait tout le bnfice du systme ! Mais il lui faut tout de mme un moyen de rcuprer le rsultat lorsqu'il aura t calcul. Le framework Executor fournit la classe Future<V>, qui reprsente un rsultat de type V dont la valeur est pour l'instant inconnue (puisque le traitement n'a pas encore t excut), mais qui sera disponible dans le futur. L'interface Future<V> propose un ensemble de mthodes permettant de rcuprer le rsultat (get()), tester sa disponibilit (isDone()), ou d'annuler son calcul (cancel()).
public interface Future<V> { public V get(); public V get(long timeout, TimeUnit unit); public boolean isDone(); public boolean cancel(boolean mayInterruptIfRunning); public boolean isCancelled(); }
La mthode get() permet de rcuprer le rsultat immdiatement s'il est disponible (c'est--dire si le traitement a bien t excut par le pool de threads). Mais attention : si son calcul n'est pas termin, la mthode est bloquante ! C'est donc une bonne pratique de vrifier la relle disponibilit du rsultat avec isDone() avant de le rcuprer.
public class FutureDemo { public static void main(String[] args) throws InterruptedException, ExecutionException { ExecutorService pool = Executors.newFixedThreadPool(4); Future<Integer> result = pool.submit(new DeepThoughtCalculator()); // Should take 7.5s pool.shutdown(); long timeBefore = System.currentTimeMillis(); while (!result.isDone()) { // Do something useful System.out.printf("Still nothing after %d ms, waiting a bit more... %n", System.currentTimeMillis() - timeBefore); // Wait a bit and retry Thread.sleep(1000);
videmment, dans une vritable application, on profitera de l'indisponibilit du rsultat pour raliser d'autres oprations utiles l'application.
Still Still Still Still Still Still Still nothing nothing nothing nothing nothing nothing nothing after after after after after after after 0 ms, waiting a bit more... 1014 ms, waiting a bit more... 2015 ms, waiting a bit more... 3015 ms, waiting a bit more... 4015 ms, waiting a bit more... 5016 ms, waiting a bit more... 6016 ms, waiting a bit more...
-8Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
Still nothing after 7016 ms, waiting a bit more... Result after 8016ms : 42
Chacun de ces traitements est coteux : 7.5 secondes pour le premier, environ 3 secondes (sur ma machine) pour le second dans notre exemple. Essayons de les lancer squentiellement :
public class SingleThreadedTest { @Test public void findAnswer() throws Exception { long timeBefore = System.currentTimeMillis(); // Asking the ancient Babylonian science for an answer
-9Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
int babylonianAnswer = new BabylonianCalculator().call(); System.out.printf("Babylonian result after %dms : %d %n", System.currentTimeMillis()-timeBefore, babylonianAnswer); // Asking a big computer for an answer int deepThoughtAnswer = new DeepThoughtCalculator().call(); System.out.printf("DeepThought result after %dms : %d %n", System.currentTimeMillis()timeBefore, deepThoughtAnswer); } } Babylonian result after 3002ms : 42 DeepThought result after 10503ms : 42 10503 ms System.out.println((System.currentTimeMillis() - timeBefore) + " ms");
Leur excution squentielle prend environ 10.5 secondes et n'utilise qu'un seul processeur :
- 10 Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
Grce au pool de threads, les deux calculs ont t mens en parallle. Le temps d'excution global a t rduit, et deux processeurs ont t utiliss :
Un problme se pose toutefois : l'ordre dans lequel les rsultats ont t rcuprs (en appelantget()) est important. Ici, nous avons - par chance! - rcupr en premier le rsultat le plus rapidement disponible. Si nous avions invers l'ordre des get(), nous aurions obtenu :
DeepThought result after 7506ms : 42 Babylonian result after 7506ms : 42 7506 ms
Bien que disponible au bout de 3 secondes seulement, le rsultat calcul par la mthode babylonienne n'a pu tre rcupr qu'au bout de 7.5 secondes !
IV-D - CompletionService
Ce problme d'ordre de disponibilit des rsultats est frquent. Pour le rsoudre, le framework Executor propose la classe CompletionService. CompletionService est un wrapper qui se branche sur un ExecutorService, et qui se charge de surveiller l'tat d'avancement des diffrents traitements qui lui ont t soumis. Sa mthode take() (bloquante) renvoie les rsultats au fur et mesure de leur disponibilit. Notez qu'il est tout de mme ncessaire de connatre le nombre de rsultats attendus.
public class CompletionServiceDemo { @Test public void findAnswer() throws ExecutionException, InterruptedException { // Wait for VM warmup Thread.sleep(2000); long timeBefore = System.currentTimeMillis(); Callable<Integer> babylonianQuestion = new BabylonianCalculator(); Callable<Integer> deepThoughtQuestion = new DeepThoughtCalculator(); // Pool with 4 threads ExecutorService pool = Executors.newFixedThreadPool(4); CompletionService<Integer> completion = new ExecutorCompletionService<Integer>(pool); completion.submit(babylonianQuestion); completion.submit(deepThoughtQuestion); pool.shutdown(); Future<Integer> answer1 = completion.take(); System.out.printf("Result 1 after %dms : %d %n", System.currentTimeMillis()-timeBefore, answer1.get()); Future<Integer> answer2 = completion.take(); System.out.printf("Result 2 after %d ms : %d %n", System.currentTimeMillis()-timeBefore, answer2.get()); long timeAfter = System.currentTimeMillis(); System.out.println((timeAfter - timeBefore) + " ms");
- 11 Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
V - Conclusion et remerciements
Dans cet article, nous avons vu les principales solutions offertes par Java pour excuter diffrents traitements de manire concurrente, et tirer le meilleur parti de la puissance de calcul disponible sur la machine. S'il est simple de crer et de lancer manuellement des threads, la solution recommande est d'utiliser le framework Executor, plus souple, plus robuste, et disposant de nombreuses fonctionnalits.
- 12 Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/
Le prochain article dcrira les problmes qui peuvent survenir lorsque plusieurs threads tentent d'accder une mme donne. Stay tuned for more happy days ! Cet article a t publi avec l'aimable autorisation de la socit Zenika, le billet original peut tre trouv sur le blog de Zenika. Nous tenons remercier ClaudeLELOUP pour sa relecture attentive de cet article et Keulkeul pour la mise au gabarit du billet original.
- 13 Copyright 2012 Olivier Croisier. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts. Cette page est dpose la SACD.
http://zenika.developpez.com/articles/java/core/javaprogconcurrente/