Introduction à la programmation concurrente en Java (1/2)


Aujourd'hui, le moindre équipement électronique - ordinateur, téléphone, tablette... - possède plusieurs coeurs, répartis sur un ou plusieurs processeurs. Si l'on souhaite en tirer le meilleur parti, il est nécessaire de se pencher sur les arcanes de la programmation concurrente. Dans cet article, nous verrons ce que sont les threads, et comment les créer et les manipuler en Java.

Mais tout d'abord, rappelons quelques notions générales.
Un processus représente l'environnement d'exécution d'un programme. Il référence d'une part un espace mémoire permettant de stocker les données propres à l'application, et d'autre part un ensemble de threads permettant l'exécution du code qui manipulera ces données.

En Java, au démarrage de l'application, un thread initial est créé : le thread "main". Son rôle est de localiser le point d'entrée de l'application (la méthode public static void main(String... args)) puis d'exécuter son code.

Ce thread, comme tous les threads, exécute la séquence d'instructions qui lui est confiée de manière purement séquentielle. Si une instruction prend du temps à compléter (par exemple, en attente de connexion à un serveur), toute l'application est paralysée.

Pour éviter cela, il est possible (et même souhaitable) de confier l'exécution de ces portions bloquantes à des threads annexes, laissant ainsi le thread principal libre de continuer l'exécution de l'application.

Voyons comment.

La classe java.lang.Thread

Créer et démarrer un thread

En Java, un thread est représenté par une instance de la classe java.lang.Thread. Le code qu'il doit exécuter est défini dans sa méthode run(), et un simple appel à la méthode start() permet de le démarrer.

public class NewThread {
 
    public static void main(String... args) {
 
        Thread t = new Thread() {
            public void run() {
                System.out.println("Je suis dans le thread : " + Thread.currentThread().getName()); // #1
            }
        };
        t.start();
 
        System.out.println("Je suis dans le thread : " + Thread.currentThread().getName()); // #2
 
    }
 
}

Ce programme produit le résultat suivant :

Je suis dans le thread : main
Je suis dans le thread : Thread-0

On voit ici que la ligne #2 a été exécutée dans le thread principal de l'application, alors que la ligne #1 a été exécutée dans un autre thread ("Thread-0").

Nommer les threads

Dans l'exemple ci-dessus, nous reconnaissons le thread "main", thread initial de l'application. Nous constatons également que notre thread annexe s'appelle "Thread-0".

Ce nom lui a été attribué automatiquement par la JVM, mais n'est pas très parlant. Pour faciliter le débuggage, il est recommandé de donner des noms explicites à nos threads, en les passant au constructeur :

Thread t = new Thread("Mon thread") { ... };

Cette modification dans notre exemple produirait alors le résultat suivant :

Je suis dans le thread : main
Je suis dans le thread : Mon thread

Start vs Run

Il est facile de confondre les méthodes run() et start() ; une erreur classique consiste à essayer de démarrer un nouveau thread en appelant la première au lieu de la seconde. Dans ce cas, le code de la méthode run() est bien exécuté, mais comme un simple appel de méthode dans le thread courant : aucun nouveau thread n'est lancé !

Il est facile de le vérifier :

public class NewThread {
 
    public static void main(String... args) {
 
        Thread t = new Thread() {
            public void run() {
                System.out.println("Je suis dans le thread : " + Thread.currentThread().getName());
            }
        };
        t.run(); // ERREUR !
 
        System.out.println("Je suis dans le thread : " + Thread.currentThread().getName());
 
    }
 
}

Nous constatons que les deux instructions println() ont été exécutées dans le même 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 démarrer un thread !

Vie et mort d'un thread

Une fois lancé, un thread "vit" jusqu'à ce qu'il ait fini d'exécuter le code de sa méthode run(). Après quoi, il est considéré comme "mort" ("terminated") et ne peut plus être redémarré.

Notez que la méthode stop() présente dans son API ne doit jamais être utilisée, car elle pourrait avoir des conséquences terribles et imprévisibles (!) sur lesquelles je ne m'étendrai pas ici.

Runnable

L'utilisation que nous faisons de la classe java.lang.Thread ci-dessus a le défaut de lier fortement la définition du traitement à exécuter (le "quoi") au thread particulier qui l'exécute (le "comment"). Que faire si l'on souhaite faire exécuter le même traitement par plusieurs threads ? Ou relancer un traitement après la mort du thread associé ? Nous comprenons ici la nécessité de découpler le "quoi" du "comment".

C'est ici qu'intervient l'interface java.lang.Runnable, qui permet d'encapsuler un traitement sous la forme d'un composant autonome et réutilisable. Pour être réellement exécuté, un Runnable doit être passé en paramètre à un Thread ou un ExecutorService (voir plus loin).

Dans l'exemple suivant, un même 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 à éxécuter, 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'exécuter les différents threads est imprévisible.

Le framework Executor

Jusqu'ici, nous avons créé et lancé manuellement un nouveau Thread à chaque fois que nous souhaitions exécuter un traitement de manière asynchrone. Si cette technique fonctionne bien pour les petites applications, elle est fortement déconseillée pour les applications d'entreprise :

  • d'une part, chaque thread supplémentaire augmente la mémoire consommée, la complexité globale de l'application, et le risque de contention ;
  • d'autre part, pour de petites tâches, le coût de création d'un nouveau thread peut se révéler supérieur au coût d'exécution du traitement associé.

Introduit avec Java 5, le framework Executor répond à ces problématiques.
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 fonctionnalités des Runnables (voir plus loin). Sa mise en oeuvre doit systématiquement être préférée à la création manuelle de threads.

Executor et ExecutorService

L'interface Executor capture l'essence même du framework Executor : l'exécution d'un traitement encapsulé dans un Runnable.

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

Je vous sens un peu déçus. Effectivement, c'est assez pauvre.

En réalité, toute la puissance du framework est exprimée dans une autre interface : ExecutorService, qui dérive d'Executor. Elle fournit des méthodes pour soumettre des traitements à exécuter (individuels ou en masse), ainsi que pour gérer 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 implémente cette interface, est tellement configurable que ses auteurs ont préféré fournir une factory couvrant les besoins les plus courants :

public class Executors {
    public ExecutorService newSingleThreadExecutor()             {...};
    public ExecutorService newFixedThreadPool(int nbThreads)     {...};	
    public ExecutorService newCachedThreadPool()                 {...};
    public ExecutorService newSingleThreadScheduledExecutor()    {...};
    public ExecutorService newScheduledThreadPool(int nbThreads) {...};
    // (...)
}

Voyons comment utiliser tout ceci pour exécuter nos traitements :

public class NewExecutorService {
 
    public static void main(String... args) {
 
        Runnable job = new Runnable() {
            public void run() {
                System.out.println("Je suis dans le thread : " + Thread.currentThread().getName());
 
            }
        };
 
        // 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());
 
    }
 
}

Le résultat est le suivant :

Je suis dans le thread : pool-1-thread-1
Je suis dans le thread : pool-1-thread-2
Je suis dans le thread : main

Afin de prouver que le pool recycle les threads au lieu d'en recréer, tentons de soumettre notre Runnable 10 fois :

Je suis dans le thread : pool-1-thread-1
Je suis dans le thread : pool-1-thread-4
Je suis dans le thread : pool-1-thread-3
Je suis dans le thread : pool-1-thread-2
Je suis dans le thread : pool-1-thread-2
Je suis dans le thread : pool-1-thread-2
Je suis dans le thread : pool-1-thread-2
Je suis dans le thread : pool-1-thread-3
Je suis dans le thread : pool-1-thread-4
Je suis dans le thread : pool-1-thread-1
Je suis dans le thread : main

Nous voyons ici que le thread "pool-1-thread-1" a été sollicité 2 fois, le thread "pool-1-thread-2" 4 fois, etc.

Callable et Future

Callable

Nous avons vu plus haut comment encapsuler un traitement dans un Runnable. Mais Runnable montre vite ses limites : comment renvoyer un résultat ? Et si le traitement lève une exception ?

Pour lever ces limitations, le framework Executor propose l'interface Callable<V>, qui est une sorte de Runnable amélioré. Le type paramétré <V> définit le type du résultat produit par la méthode call(). Ainsi, un Callable<Integer> produira un Integer.

public interface Callable<V> {
	V call() throws Exception;
}

Un Callable est prévu pour être soumis à la méthode submit() d'un pool de threads, qui exécutera son traitement de manière 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());
 
    }
 
}

Le résultat est le suivant :

Je suis dans le thread : pool-1-thread-1
Je suis dans le thread : main
Future

Une fois soumis à un pool de threads, un Callable est généralement[1] exécuté de manière asynchrone ; le résultat 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 résultat de manière synchrone : il perdrait tout le bénéfice du système ! Mais il lui faut tout de même un moyen de récupérer le résultat lorsqu'il aura été calculé.

Le framework Executor fournit la classe Future<V>, qui représente un résultat de type V dont la valeur est pour l'instant inconnue (puisque le traitement n'a pas encore été exécuté), mais qui sera disponible dans le futur.

L'interface Future<V> propose un ensemble de méthodes permettant de récupérer le résultat (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 méthode get() permet de récupérer le résultat immédiatement s'il est disponible (c'est-à-dire si le traitement a bien été exécuté par le pool de threads). Mais attention : si son calcul n'est pas terminé, la méthode est bloquante ! C'est donc une bonne pratique de vérifier la réelle disponibilité du résultat avec isDone() avant de le récupérer.

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);
        }
 
        Integer answer = result.get();
        System.out.printf("Result after %dms : %d %n", System.currentTimeMillis()-timeBefore, answer);
 
    }
 
}

Evidemment, dans une véritable application, on profitera de l'indisponibilité du résultat pour réaliser d'autres opérations utiles à l'application.

Still nothing after 0 ms, waiting a bit more... 
Still nothing after 1014 ms, waiting a bit more... 
Still nothing after 2015 ms, waiting a bit more... 
Still nothing after 3015 ms, waiting a bit more... 
Still nothing after 4015 ms, waiting a bit more... 
Still nothing after 5016 ms, waiting a bit more... 
Still nothing after 6016 ms, waiting a bit more... 
Still nothing after 7016 ms, waiting a bit more... 
Result after 8016ms : 42

ExecutorService en action

Je suis sûr que vous brûlez de connaître la réponse à "La grande question sur la vie, l'univers et le reste" ?

Deux solutions s'offrent à nous :

  • La demander directement à Deep Thought - mais le calcul risque de prendre 7.5 millions d'années.
  • La calculer selon la méthode des anciens Babyloniens, en espérant qu'elle soit plus rapide.

Encapsulons ces deux traitements dans des Callable :

public class DeepThoughtCalculator implements Callable<Integer> {
 
    @Override
    public Integer call() throws Exception {
        Util.busyWait(7500, TimeUnit.MILLISECONDS); // Busy wait
        return 42;
    }
 
}
public class BabylonianCalculator implements Callable<Integer> {
 
    private static final int MAGIC_NUMBER = 1764;
 
    @Override
    public Integer call() throws Exception {
        int number = MAGIC_NUMBER;
        double approximate = getApproximate(number);
        while (!isPreciseEnough(number, approximate)) {
            approximate = 0.5 * (approximate + (number / approximate));
            Util.busyWait(250, TimeUnit.MILLISECONDS);
        }
        return (int) approximate;
    }
 
    private double getApproximate(int number) {
        int digits = Integer.toString(number).length();
        double approximate = Math.pow(10, digits);
        return approximate *= number % 2 == 0 ? 2 : 6;
    }
 
    private boolean isPreciseEnough(int number, double approximate) {
        return Math.abs(number - (approximate * approximate)) < 1;
    }
 
}

Chacun de ces traitements est coûteux : 7.5 secondes pour le premier, environ 3 secondes (sur ma machine) pour le second dans notre exemple.

Essayons de les lancer séquentiellement :

public class SingleThreadedTest {
 
    @Test
    public void findAnswer() throws Exception {
 
        long timeBefore = System.currentTimeMillis();
 
        // Asking the ancient Babylonian science for an answer
        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);
 
        System.out.println((System.currentTimeMillis() - timeBefore) + " ms");
    }
 
}
Babylonian result after 3002ms : 42 
DeepThought result after 10503ms : 42 
10503 ms

Leur exécution séquentielle prend environ 10.5 secondes et n'utilise qu'un seul processeur :


single_threaded.png

Essayons maintenant de les soumettre à un ExecutorService :

public class MultithreadedTest {
 
    @Test
    public void findAnswer() throws ExecutionException, InterruptedException {
 
        long timeBefore = System.currentTimeMillis();
 
        Callable<Integer> babylonianQuestion = new BabylonianCalculator();
        Callable<Integer> deepThoughtQuestion = new DeepThoughtCalculator();
 
        // Pool with 4 threads
        ExecutorService pool = Executors.newFixedThreadPool(4);
        Future<Integer> babylonianFuture = pool.submit(babylonianQuestion);
        Future<Integer> deepThoughtFuture = pool.submit(deepThoughtQuestion);
        pool.shutdown();
 
        // Get result 1
        Integer babylonianAnswer = babylonianFuture.get();
        System.out.printf("Babylonian result after %dms : %d %n", System.currentTimeMillis()-timeBefore, babylonianAnswer);
 
        // Get result 2
        Integer deepThoughtAnswer = deepThoughtFuture.get();
        System.out.printf("DeepThought result after %dms : %d %n", System.currentTimeMillis()-timeBefore, deepThoughtAnswer);
 
        long timeAfter = System.currentTimeMillis();
        System.out.println((timeAfter - timeBefore) + " ms");
 
    }
 
}
Babylonian result after 3004ms : 42 
DeepThought result after 7504ms : 42 
7504 ms

Grâce au pool de threads, les deux calculs ont été menés en parallèle. Le temps d'exécution global a été réduit, et deux processeurs ont été utilisés :


Multithreade_1.png

Un problème se pose toutefois : l'ordre dans lequel les résultats ont été récupérés (en appelantget()) est important. Ici, nous avons - par chance! - récupéré en premier le résultat 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 résultat calculé par la méthode babylonienne n'a pu être récupéré qu'au bout de 7.5 secondes !

CompletionService

Ce problème d'ordre de disponibilité des résultats est fréquent. Pour le résoudre, 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 différents traitements qui lui ont été soumis. Sa méthode take() (bloquante) renvoie les résultats au fur et à mesure de leur disponibilité.

Notez qu'il est tout de même nécessaire de connaître le nombre de résultats 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");
 
    }
 
}
Result 1 after 3005ms : 42 
Result 2 after 7505 ms : 42 
7505 ms

InvokeAny : que le meilleur gagne !

Pour finir, voyons une fonctionnalité moins connue de l'ExecutorService : la méthode invokeAny().

Il arrive que plusieurs traitements (algorithmes ou sous-systèmes de l'applications) soient en compétition pour produire un résultat donné - par exemple, récupérer une librairie à partir de l'un des nombreux repositories Maven distants. Nous souhaiterions alors pouvoir tout arrêter dès que l'un des traitements renvoie le résultat souhaité.

La méthode invokeAny() prend en paramètre une collection de traitements de même type, et renvoie le résultat fourni par le traitement le plus rapide. Attention toutefois, invokeAny() est une méthode bloquante.

Dans notre exemple, deux algorithmes sont en compétition pour calculer la réponse à "La grande question sur la vie, l'univers et le reste", mais une seule réponse nous suffit. Que le meilleur gagne !

public class MultithreadedTest2 {
 
    @Test
    public void findAnswer() throws ExecutionException, InterruptedException {
 
        // Wait for VM warmup
        Thread.sleep(1000);
 
        long timeBefore = System.currentTimeMillis();
 
        Callable<Integer> deepThoughtQuestion = new DeepThoughtCalculator();
        Callable<Integer> babylonianQuestion = new BabylonianCalculator();
 
        // Submit many jobs, but waiting for only 1 answer
        ExecutorService pool = Executors.newFixedThreadPool(4);
        Integer answer = pool.invokeAny(Arrays.asList(deepThoughtQuestion, babylonianQuestion));
        pool.shutdown();
 
        System.out.printf("First result after %dms : %d %n", System.currentTimeMillis()-timeBefore, answer);
 
        long timeAfter = System.currentTimeMillis();
        System.out.println((timeAfter - timeBefore) + " ms");
 
    }
 
}
First result after 3004ms : 42 
3004 ms

Conclusion

Dans ce premier article, nous avons vu les principales solutions offertes par Java pour exécuter différents traitements de manière concurrente, et tirer le meilleur parti de la puissance de calcul disponible sur la machine.

S'il est simple de créer et de lancer manuellement des threads, la solution recommandée est d'utiliser le framework Executor, plus souple, plus robuste, et disposant de nombreuses fonctionnalités.

Le prochain article décrira les problèmes qui peuvent survenir lorsque plusieurs threads tentent d'accéder à une même donnée.
Stay tuned for more happy days !

Note

[1] Cela dépend du type de thread pool ; il est possible que l'exécution soit synchrone.


Commentaires

1. Le mercredi 11 avril 2012, 09:59 par emmanuel lécharny

commentaire

Intro : un processus n'a pas de threads. Un processus, c'est de la mémoire, et du code. Un thread est une forme de processus 'léger' (il partage la mémoire avec d'autres threads)

Sinon, super clair.

J'aurai inversé la présentation de Executor et de Threads, pour bien indiquer que Executor est la méthode à utiliser si on veut commencer la programmation concurrente en Java (Thread étant alors présenter comme de la plomberie...).

Il faudrait aussi expliquer plus en détail la notion de Future qui est assez fondamentales et peu évidente à comprendre.

Sinon, le coût de création et d'exécution d'un thread en Java n'est pas forcément rédhibitoire. Il est par exemple possible de créer plusieurs milliers de threads sans que cela soit réellement pénalisant (tout dépend ce que l'on veut en faire, bien sûr !) (http://paultyma.blogspot.fr/2008/03... http://mailinator.blogspot.fr/2008/...)

2. Le mercredi 11 avril 2012, 10:32 par Jocelyn Lecomte

Très bon article, moi qui suis toujours un peu intimidé et barbé à la fois par ces concepts, j'ai réussi à tout comprendre et à lire jusqu'à la fin :)
Néanmoins, quelques précisions sur pool.shutdown() auraient été les bienvenues en ce qui me concerne.

3. Le mercredi 11 avril 2012, 10:34 par Bruno

Très clair et concis : excellent.
J'aurais juste souligné que CompletionService et InvokeAny s'adressent à une catégorie de problèmes qui renvoient des résultats du même type.
Avant cela, les job sont libres de renvoyer des types différents (même si dans l'exemple ils renvoient toujours un Integer).

4. Le mercredi 11 avril 2012, 11:26 par Sam

Un topo sur les constructs des barrières de synchronisation (countdownlatch, phasers, cyclicbarriers), peut être interessant pour compléter cette belle intro.
Le problème avec la programmation concurrente, est que c'est un domaine qui n'arrête pas d'évoluer notamment sur l JVM, et même en Java, du coup c'est difficile de fixer la limite d'une introduction.
Bon boulot tout de même, je pense que c'est un minimum requis pour tout developpeur Java aujourd'hui de connaitre ces concepts.

5. Le mercredi 11 avril 2012, 14:57 par louis gueye

Toujours aussi clair, un vrai plaisir à lire.
Merci à l'auteur.
On reconnaît tous la différence entre un article "copier-coller" et un article dont le contenu nous apporte vraiment.

6. Le jeudi 12 avril 2012, 12:37 par fabszn

Hello,

Très bon article! intéressant et surtout très clair.

Merci à l'auteur!

7. Le mercredi 25 avril 2012, 22:36 par Stéphane Nicolas

Excellent article. J'aurais dû le lire avant ma dernière entrevue. :)
Je n'avais jamais pris le temps de lire un article sur les nouveaux mécanismes permettant l'exécution asynchrone de code et celui-ci m'a fait comprendre très rapidement des notions qui piquaient ma curiosité depuis longtemps.

Merci.

8. Le mardi 28 août 2012, 15:00 par stan

Bravo. Débutant dans le monde web, cet article très pédagogique, m' a permis de bien comprendre les concepts de Thread, Runnable et Executor. Il manque, peut-être dans cet article, un exemple avec des données d'entrée pour être utilisé par la méthode call().
Je suis très intéressé par l’article traitant les problèmes qui peuvent survenir lorsque plusieurs threads tentent d’accéder à une même donnée. Pouvez-vous me faire suivre cet article ou son lien.

9. Le lundi 14 janvier 2013, 16:29 par Boukhezar

c'est un très bon article, qui m'a éclairé les différent concepts, mais j'aurai aimé des explications sur "newCachedThreadPool()", tout simplement car l'allocation des threads est faite automatiquement, alors qu'avec newFixedThreadPool() on fixe le nombre de thread.
la question qui se pose, si notre programme web est polyvalent pour chaque exécution , comment fixer le nombre de thread ?

10. Le mercredi 23 octobre 2013, 09:20 par nizar

Bonjour,
trés bon tuto mais c'est quoi Util.busyWait ?
ça provient de quelle classe ? quelle import à utiliser ?

Fil des commentaires de ce billet

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.