I. Présentation▲
Les ordinateurs ont vu leur nombre de processeurs/cœurs augmentés grandement ces dernières années afin de compenser les limites d'augmentation de puissance d'un seul processeur. Afin d'exploiter pleinement cette puissance de calcul, il convient de réaliser des applications capables d'effectuer plusieurs tâches en parallèle. Et même si les systèmes d'exploitation sont depuis longtemps multitâches, Java demeure un langage séquentiel à sa base.
Depuis longtemps, les développeurs utilisent deux techniques pour rendre leurs applications multitâches : le fork ou les threads. En Java, c'est cette dernière technique qui est utilisée. Contrairement au fork, les threads partagent la même mémoire.
Partageant ainsi des ressources (en mémoire), les threads risquent alors de rentrer en concurrence et de corrompre le système. C'est alors qu'intervient la programmation concurrente qui rassemble un ensemble de fonctionnalités et de techniques pour permettre la synchronisation de tâches s'exécutant en parallèle.
Cet article s'attache uniquement à la synchronisation des ressources en mémoire. En effet, les autres ressources étant partagées également par d'autres processus (éventuellement sur d'autres machines), les outils et techniques sont souvent propres à chaque ressource (base de données, disque, etc.).
Si ces ressources sont accédées uniquement par plusieurs threads du même programme Java, les outils et techniques décrits ci-après s'appliquent dans la mesure où vous créez un objet unique pour chacune de ces ressources.
II. Modèle▲
En Java, un thread est représenté par la classe java.lang.Thread. Les threads disposent de plusieurs propriétés :
- id : un numéro unique ;
- name : le nom du thread. Il n'est pas nécessairement unique ;
- priority : la machine virtuelle exécute prioritairement les threads avec la plus grande valeur ;
- state : l'état du thread ;
- threadGroup : permet d'organiser les threads ;
- daemon : indique si le thread est une tâche de fond (démon). La VM s'arrête automatiquement dès lors qu'aucun thread principal (non démon) n'est en cours d'exécution.
Les groupes de threads forment une arborescence. Lors de sa création, par défaut, un groupe appartient au groupe du thread en cours et hérite également de ses propriétés. Il en est de même pour les threads qui héritent des propriétés du thread en cours.
Pour exécuter du code dans un thread, il existe deux techniques :
-
Créer une classe fille de Thread et surcharger la méthode run() :
LoggerSélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.//com.developpez.lmauzaize.java.concurrence.Logger
public
class
Logger{
private
static
final
long
startTime=
System.currentTimeMillis
(
);private
static
final
TimeZone UTC=
TimeZone.getTimeZone
(
"UTC"
);public
static
final
UncaughtExceptionHandler exceptionHandler=
new
UncaughtExceptionHandler
(
){
@Override
public
void
uncaughtException
(
Thread t, Throwable e){
printStackTrace
(
t, e);}
public
StringtoString
(
){
return
"Logger.printStackTrace(Thread,Throwable)"
;}
}
;private
Logger
(
){}
public
static
void
println
(
String msg, Object... params){
println
(
Thread.currentThread
(
), msg, params);}
public
static
void
println
(
Thread thread, String msg, Object... params){
// Message
String format=
"%tT.%<tL [%-15s] "
+
msg+
"%n"
;// Paramètres
Object[] args=
new
Object[2
+
(
params!=
null
? params.length :0
)]; Calendar cal=
Calendar.getInstance
(
UTC); cal.setTimeInMillis
(
System.currentTimeMillis
(
)-
startTime); args[0
]=
cal; args[1
]=
Thread.currentThread
(
).getName
(
);if
(
args.length>
2
){
System.arraycopy
(
params,0
, args,2
, params.length);}
System.out.printf
(
format, args);}
public
static
void
printStackTrace
(
Throwable t){
printStackTrace
(
Thread.currentThread
(
), t);}
public
static
void
printStackTrace
(
Thread thread, Throwable t){
StringWriter writer=
new
StringWriter
(
); t.printStackTrace
(
new
PrintWriter
(
writer,true
));println
(
thread,"%s%n%s"
, t, writer);}
}
La classe Logger sera utilisée tout au long de cet article pour faciliter l'affichage des traces sur la sortie standard. Vous noterez également que la première ligne est un commentaire indiquant le nom complet de la classe dans les sources de l'article.
Nouvelle classe fille de ThreadSélectionnez1.
2.
3.
4.
5.
6.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.FilleThread
new
Thread
(
){
public
void
run
(
){
Logger.println
(
"Je suis un nouveau thread !"
);}
}
.start
(
); -
Créer une instance de Runnable et passer cette instance en paramètre lors de la création du thread. Cette dernière technique permet d'exécuter plusieurs fois le même code :
Nouvelle classe RunnableSélectionnez1.
2.
3.
4.
5.
6.
7.
8.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.NouveauRunnable
Runnable runnable=
new
Runnable
(
){
public
void
run
(
){
Logger.println
(
"Je suis un nouveau thread !"
);}
}
;new
Thread
(
runnable).start
(
);new
Thread
(
runnable).start
(
);
Certaines parties de l'API utilisent une classe similaire à Runnable qui permet à une tâche de renvoyer un résultat, il s'agit de l'interface Callable.
Pour terminer cette présentation des spécifications Java, il est important de noter que toute instance peut servir de verrou. Ce qui nous amène à présenter les blocs synchronized et les méthodes wait/notify.
III. Gestion des threads▲
La section Modèle présentait le fonctionnement de base des threads avec Java. Cette section vise à présenter les solutions proposées par l'API pour gérer l'exécution de tâches sans avoir à gérer manuellement les threads.
III-A. ThreadFactory▲
Bien que la gestion des threads puisse être déléguée à un service dédié (voir ci-après), leur création fait l'objet d'une interface particulière appelée ThreadFactory. Cette interface permet de spécifier les paramètres des threads à créer : groupe, nom, priorité, démon, etc.
L'API fournit deux méthodes pour créer des ThreadFactory :
-
defaultThreadFactory : créer des threads avec les caractéristiques décrites ci-dessous :
- Nom : de la forme « pool-<N>-thread-<M> ». « N » étant un numéro séquentiel de fabrique et « M » un numéro séquentiel de thread au sein de la fabrique,
- Groupe : le groupe est déterminé à la création de la fabrique. S'il y a un gestionnaire de sécurité, c'est son groupe qui est utilisé ; sinon c'est celui du thread courant,
- Démon : la fabrique ne produit que des threads non démons,
- Priorité : la priorité est normale sauf si le maximum toléré par le groupe est inférieur.
Executors.defaultThreadFactory - démo (code)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.threadfactory.DefaultThreadFactoryDemo
public
static
ThreadcréerThreadLimité
(
Runnable tâche, String name){
ThreadGroup groupe=
new
ThreadGroup
(
"groupe"
); groupe.setDaemon
(
true
); groupe.setMaxPriority
(
Thread.MIN_PRIORITY); Thread thread=
new
Thread
(
groupe, tâche, name); thread.setDaemon
(
true
);class
FauxClassLoaderextends
ClassLoader{}
thread.setContextClassLoader
(
new
FauxClassLoader
(
)); thread.setUncaughtExceptionHandler
(
Logger.exceptionHandler);return
thread;}
public
static
void
affiche
(
String cas, Thread){
Logger.println
(
""
); Logger.println
(
cas); Logger.println
(
" nom =%s"
, thread.getName
(
)); Logger.println
(
" groupe =%s"
, thread.getThreadGroup
(
).getName
(
)); Logger.println
(
" démon =%s"
, thread.isDaemon
(
)); Logger.println
(
" priorité=%s"
, thread.getPriority
(
)); Logger.println
(
" loader =%s"
, thread.getContextClassLoader
(
)); Logger.println
(
" handler =%s"
, thread.getUncaughtExceptionHandler
(
));}
public
static
void
main
(
String[] args)throws
InterruptedException{
ThreadFactory defaultThreadFactory=
Executors.defaultThreadFactory
(
); ThreadFactory customThreadFactory=
Thread::new
; Runnable tâche=
(
)->
{
affiche
(
"defaultThreadFactory"
, defaultThreadFactory.newThread
(
null
));affiche
(
"customThreadFactory"
, customThreadFactory.newThread
(
null
));}
;// Depuis "main"
tâche.run
(
);// Depuis un nouveau thread
Thread defaultThread=
defaultThreadFactory.newThread
(
tâche); defaultThread.start
(
); defaultThread.join
(
);// Depuis un groupe restreint
Thread thread=
créerThreadLimité
(
tâche,"thread"
); thread.start
(
); thread.join
(
);}
Executors.defaultThreadFactory - démo (console)Cacher/Afficher le codeSélectionnez -
privilegedThreadFactory : même chose que précédemment avec le ClassLoader et les mêmes permissions que le thread ayant créé la fabrique.
Executors.privilegedThreadFactory - démo (code)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.threadfactory.PrivilegedThreadFactoryDemo
// Vérifie si le thread possède les permissions
static
boolean
possèdePermission
(
);// Effectue une action avec les permissions
static
<
T>
TavecPermission
(
PrivilegedAction<
T>
action);public
static
void
afficheAvecPermission
(
String cas){
affiche
(
cas, Thread.currentThread
(
)); Logger.println
(
" permis =%s"
,possèdePermission
(
));}
class
Executeurimplements
Runnable{
Map<
String, ThreadFactory>
threadFactories=
new
LinkedHashMap<>(
);Executeur
(
){
threadFactories.put
(
"Thread.new"
, Thread::new
); threadFactories.put
(
"defaultThreadFactory"
, Executors.defaultThreadFactory
(
)); threadFactories.put
(
"privilegedThreadFactory"
, Executors.privilegedThreadFactory
(
));}
public
void
run
(
){
for
(
Entry<
String, ThreadFactory>
threadFactory : threadFactories.entrySet
(
)){
Thread thread=
threadFactory.getValue
(
).newThread
((
)->
afficheAvecPermission
(
threadFactory.getKey
(
))); thread.start
(
); thread.join
(
);}
}
}
Logger.println
(
""
); Logger.println
(
""
); Logger.println
(
"******************************"
); Logger.println
(
"Sans permission"
);new
Executeur
(
).run
(
); Logger.println
(
""
); Logger.println
(
""
); Logger.println
(
"******************************"
); Logger.println
(
"Avec permission"
); Executeur executeur=
avecPermission
(
Executeur::new
); Thread thread=
créerThreadLimité
(
executeur,"tâche"
); thread.start
(
); thread.join
(
);Executors.privilegedThreadFactory - démo (console)Cacher/Afficher le codeSélectionnez
III-B. ExecutorService▲
L'interface Executor désigne un service capable de traiter des tâches (Runnable). Elle est étendue par l'interface ExecutorService qui permet de suivre l'évolution du traitement des tâches via des Future (voir ci-après).
Pour créer une instance d'ExecutorService, le plus simple est d'utiliser la classe utilitaire Executors :
-
newCachedThreadPool : utilise un pool de threads qui grandit en fonction des besoins. Les threads créés sont conservés jusqu'à 60 secondes d'inactivité. Il est donc idéal pour exécuter plusieurs tâches courtes, mais pas pour exécuter un grand nombre de tâches en parallèle, en effet il risque de saturer le système par la création d'un trop grand nombre de threads.
ThreadFactory - loggerCacher/Afficher le codeSélectionnezExecutors.newCachedThreadPool - démo (code)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.executors.NewCachedThreadPoolDemo
int
parallele=
4
; ExecutorService executor=
Executors.newCachedThreadPool
(
new
ThreadFactoryLogger
(
));for
(
int
i=
1
; i<=
parallele; i++
){
String nom=
String.format
(
"Tache-%02d"
, i); Runnable tache=
new
Runnable
(
){
public
void
run
(
){
Logger.println
(
"[%s] Début"
, nom);int
pause=
ThreadLocalRandom.current
(
).nextInt
(
2_000
)+
200
; Logger.println
(
"[%s] Pause %dms"
, nom, pause); TimeUnit.MILLISECONDS.sleep
(
pause); Logger.println
(
"[%s] Fin"
, nom);}
}
; executor.submit
(
tache);}
Executors.newCachedThreadPool - démo (console)Cacher/Afficher le codeSélectionnez -
newFixedThreadPool : utilise un pool de threads de taille fixe. Les threads sont démarrés au fur et à mesure des besoins jusqu'à ce que le pool atteigne sa taille maximale. Les threads ainsi démarrés vivent tant que le service n'est pas arrêté. Si tous les threads sont occupés, les tâches sont mises en attente dans une file pour une exécution ultérieure.
Executors.newFixedThreadPool - démo (code)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.executors.FixedThreadPoolDemo
int
parallele=
4
; Logger.println
(
"Lancement du pool"
); ExecutorService executor=
Executors.newFixedThreadPool
(
parallele,new
ThreadFactoryLogger
(
));for
(
int
i=
1
; i<=
parallele*
2
; i++
){
String nom=
String.format
(
"Tache-%02d"
, i); Logger.println
(
"Soumission de la tâche %s"
, nom);int
nb=
i; Runnable tache=
new
Runnable
(
){
public
void
run
(
){
int
pause=
ThreadLocalRandom.current
(
).nextInt
(
2_000
)+
1_500
+
(
parallele*
2
-
nb)*
500
; Logger.println
(
"[%s] Pause %dms"
, nom, pause); TimeUnit.MILLISECONDS.sleep
(
pause); Logger.println
(
"[%s] Fin"
, nom);}
}
; executor.submit
(
tache); TimeUnit.MILLISECONDS.sleep
(
500
);}
TimeUnit.SECONDS.sleep
(
12
); executor.shutdown
(
);}
Executors.newFixedThreadPool - démo (console)Cacher/Afficher le codeSélectionnez -
newSingleThreadExecutor : même chose que précédemment avec une taille de 1, excepté que le service ne peut être reconfiguré (par exemple en transtypant).
Executors.newSingleThreadExecutor - démo (code)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.executors.SingleThreadPoolDemo
ExecutorService executor=
Executors.newSingleThreadExecutor
(
new
ThreadFactoryLogger
(
)); Logger.println
(
"Description du service:"
);afficherMembres
(
executor.getClass
(
)); Logger.println
(
""
); Random random=
new
Random
(
);for
(
int
i=
1
; i<=
4
; i++
){
String nom=
String.format
(
"Tache-%02d"
, i); Runnable tache=
new
Runnable
(
){
public
void
run
(
){
int
pause=
random.nextInt
(
2_000
); Logger.println
(
"[%s] Pause %dms"
, nom, pause); TimeUnit.MILLISECONDS.sleep
(
pause); Logger.println
(
"[%s] Fin"
, nom);}
}
; executor.submit
(
tache);}
executor.shutdown
(
);Executors.newSingleThreadExecutor - démo (console)Cacher/Afficher le codeSélectionnez
Sachez qu'il existe une interface fille de ExecutorService qui permet également la planification : ScheduledExecutorService. Ce type de service permet de retarder l'exécution d'une tâche et/ou de les exécuter de manière périodique.
III-C. Future▲
Un Future représente une tâche à exécuter et permet de récupérer son résultat via la méthode get() :
-
il peut s'agit de la valeur renvoyée :
Future - Récupération du résultat (code)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.future.FutureRécupérationRésultat
Callable<
Thread>
tâche=
new
Callable<
Thread>(
){
public
Threadcall
(
)throws
Exception{
Logger.println
(
"Début"
); Thread.sleep
(
2_000
); Logger.println
(
"Fin"
);return
Thread.currentThread
(
);}
}
; ExecutorService executor=
Executors.newSingleThreadExecutor
(
); Future<
Thread>
future=
executor.submit
(
tâche); Thread thread=
future.get
(
); Logger.println
(
"La tâche a été exécutée sur %s"
, thread.getName
(
)); executor.shutdown
(
);Future - Récupération du résultat (console)Cacher/Afficher le codeSélectionnez -
ou, il peut s'agir de l'exception levée :
Future - Récupération de l'exception (code)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.future.FutureRécupérationException
class
MonExceptionextends
Exception{}
Callable<
Thread>
tâche=
new
Callable<
Thread>(
){
public
Threadcall
(
)throws
Exception{
Logger.println
(
"Début"
); Logger.println
(
"Fin"
);throw
new
MonException
(
);}
}
; ExecutorService executor=
Executors.newSingleThreadExecutor
(
);try
{
Future<
Thread>
future=
executor.submit
(
tâche); Thread thread=
future.get
(
); Logger.println
(
"La tâche a été exécutée sur %s"
, thread);}
catch
(
ExecutionException e){
Logger.printStackTrace
(
e);}
finally
{
executor.shutdown
(
);}
Future - Récupération de l'exception (console)Cacher/Afficher le codeSélectionnez
Sachez qu'il est également possible d'attendre le résultat pendant un temps défini. Une TimeoutException est levée si l'attente expire.
La méthode cancel permet d'annuler une tâche qui a été soumise.
-
Si la tâche est dans la file d'attente, la tâche est simplement retirée de la file et n'est jamais exécutée.
Future - Annulation lors de l'attente (code)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.future.FutureAnnulationAttente
Callable<
Thread>
tâche=
new
Callable<
Thread>(
){
public
Threadcall
(
)throws
Exception{
Logger.println
(
"[Tâche] Début"
); Logger.println
(
"[Tâche] Fin"
);return
Thread.currentThread
(
);}
}
; Callable<
Thread>
attente=
new
Callable<
Thread>(
){
public
Threadcall
(
)throws
Exception{
Logger.println
(
"[Attente] Début"
); Thread.sleep
(
3_000
); Logger.println
(
"[Attente] Fin"
);return
Thread.currentThread
(
);}
}
; ExecutorService executor=
Executors.newSingleThreadExecutor
(
);// Monopolisation du service
executor.submit
(
attente);// Soumission de la tâche à annuler
Future<
Thread>
future=
executor.submit
(
tâche);// Programme l'annulation
new
Thread
(
"Annulation"
){
public
void
run
(
){
try
{
Thread.sleep
(
2_000
);}
catch
(
InterruptedException e){
Logger.printStackTrace
(
e);}
finally
{
Logger.println
(
"Annulation de la tâche (%s)"
, future.cancel
(
false
));}
}
;}
.start
(
);// Récupération du résultat
try
{
Thread thread=
future.get
(
); Logger.println
(
"La tâche a été exécutée sur %s"
, thread);}
catch
(
CancellationException e){
Logger.println
(
"La tâche a été annulée"
);}
executor.shutdown
(
);Future - Annulation lors de l'attente (console)Cacher/Afficher le codeSélectionnez -
Si la tâche est déjà en cours d'exécution et qu'il n'y a pas de demande d'interruption, la tâche continue son exécution, mais il n'est plus possible de récupérer son résultat.
Future - Annulation sans interruption (code)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.future.FutureAnnulationSansInterruption
Callable<
Thread>
tâche=
new
Callable<
Thread>(
){
public
Threadcall
(
)throws
Exception{
Logger.println
(
"[Attente] Début"
); Thread.sleep
(
5_000
); Logger.println
(
"[Attente] Fin"
);return
Thread.currentThread
(
);}
}
; ExecutorService executor=
Executors.newSingleThreadExecutor
(
);// Soumission de la tâche à annuler
Future<
Thread>
future=
executor.submit
(
tâche);// Programme l'annulation
Thread.sleep
(
2_000
); Logger.println
(
"Annulation de la tâche (%s)"
, future.cancel
(
false
));// Récupération du résultat
try
{
Thread thread=
future.get
(
); Logger.println
(
"La tâche a été exécutée sur %s"
, thread);}
catch
(
CancellationException e){
Logger.println
(
"La tâche a été annulée"
);}
executor.shutdown
(
);Future - Annulation sans interruption (console)Cacher/Afficher le codeSélectionnez -
S'il y a en plus une demande d'interruption, le thread exécutant la tâche est interrompu. D'une part, la tâche est libre de traiter cette interruption comme il lui convient ; d'autre part, le thread continuera à exécuter les tâches qui suivent.
Future - Annulation avec interruption (code)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.future.FutureAnnulationAvecInterruption
Callable<
Thread>
tâche=
new
Callable<
Thread>(
){
public
Threadcall
(
)throws
Exception{
Logger.println
(
"[Attente] Début"
);try
{
Thread.sleep
(
5_000
);}
catch
(
InterruptedException e){
Logger.println
(
"[Attente] La tâche a été annulée"
);}
Logger.println
(
"[Attente] Fin"
);return
Thread.currentThread
(
);}
}
; Callable<
Thread>
suite=
new
Callable<
Thread>(
){
public
Threadcall
(
)throws
Exception{
Logger.println
(
"[Suite] Début"
); Logger.println
(
"[Suite] Fin"
);return
Thread.currentThread
(
);}
}
; ExecutorService executor=
Executors.newSingleThreadExecutor
(
);// Soumission des tâches
Future<
Thread>
future1=
executor.submit
(
tâche); Future<
Thread>
future2=
executor.submit
(
suite);// Programme l'annulation
Thread.sleep
(
2_000
); Logger.println
(
"Annulation de la tâche [Attente](%s)"
, future1.cancel
(
true
));// Récupération des résultats
try
{
Thread thread=
future1.get
(
); Logger.println
(
"La tâche [Attente] a été exécutée sur %s"
, thread.getName
(
));}
catch
(
CancellationException e){
Logger.println
(
"La tâche [Attente] a été annulée"
);}
Thread thread=
future2.get
(
); Logger.println
(
"La tâche [Suite ] a été exécutée sur %s"
, thread.getName
(
)); executor.shutdown
(
);Future - Annulation avec interruption (console)Cacher/Afficher le codeSélectionnez
III-D. Fork/Join (1.7+)▲
Le framework Fork/Join est l'une des grandes nouveautés de Java 7. Il repose sur le principe « Diviser pour régner », c'est-à-dire découper le travail en petites tâches. Mais plutôt que découper le travail en tout petits morceaux que devront se disputer les threads, le principe est de diviser le traitement si la taille du problème est trop importante :
2.
3.
4.
5.
6.
si (taille non critique) {
<exécuter le traitement>
} sinon {
<découper le traitement en deux parties>
<lancer l'exécution des deux parties>
}
Avec l'API Fork/Join, cela donne un gabarit de cette forme :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
abstract
class
MonAction extends
java.util.concurrent.RecursiveAction {
abstract
boolean
estTailleCritique
(
);
abstract
void
avant
(
);
abstract
void
traitement
(
);
abstract
void
après
(
);
abstract
java.util.concurrent.ForkJoinTask<
Void>
partie
(
int
n);
@Override
protected
void
compute
(
) {
avant
(
);
if
(!
estTailleCritique
(
)) {
traitement
(
);
}
else
{
invokeAll
(
partie
(
1
), partie
(
2
));
}
après
(
);
}
}
Concrètement, l'API se base sur la classe ForkJoinTask. Il est en revanche recommandé d'étendre RecursiveAction si votre traitement ne renvoie pas de résultat (void), sinon RecursiveTask.
Pour la démonstration, je vous propose un petit programme qui va simplement remplir un tableau d'entiers avec des nombres aléatoires :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinDemo
class
Randomizer {
int
[] table =
new
int
[1024
*
1024
*
256
];
void
random
(
int
start, int
end) {
ThreadLocalRandom random =
ThreadLocalRandom.current
(
);
for
(
int
i =
start; i <=
end; i++
) {
table[i] =
random.nextInt
(
);
}
}
void
randomAll
(
) {
random
(
0
, table.length -
1
);
table =
null
;
}
}
class
RandomizerForkJoinAction extends
RecursiveAction {
Randomizer randomizer;
int
start, end;
RandomizerForkJoinAction
(
) {
this
.randomizer =
new
Randomizer
(
);
this
.start =
0
;
this
.end =
randomizer.table.length -
1
;
}
RandomizerForkJoinAction
(
Randomizer randomizer, int
start, int
end) {
this
.randomizer =
randomizer;
this
.start =
start;
this
.end =
end;
}
@Override
protected
void
compute
(
) {
if
((
end -
start) <=
1024
*
1024
) {
randomizer.random
(
start, end);
}
else
{
int
mid =
(
this
.end +
this
.start) /
2
;
invokeAll
(
new
RandomizerForkJoinAction
(
randomizer, start, mid),
new
RandomizerForkJoinAction
(
randomizer, mid+
1
, end)
);
}
}
}
long
start;
Logger.println
(
"Monothread - début"
);
start =
System.currentTimeMillis
(
);
new
Randomizer
(
).randomAll
(
);
Logger.println
(
"Monothread - fin (%d ms)"
, System.currentTimeMillis
(
) -
start);
Logger.println
(
"Fork/Join - début"
);
start =
System.currentTimeMillis
(
);
new
ForkJoinPool
(
).invoke
(
new
RandomizerForkJoinAction
(
));
Logger.println
(
"Fork/Join - fin (%d ms)"
, System.currentTimeMillis
(
) -
start);
Mais pourquoi invoquer les sous-tâches alors que le thread courant va être en attente de leur résultat ? Et bien, c'est ce que va faire la méthode invokeAll ! Elle va mettre en file les tâches sauf une et l'exécuter, puis attendre que les sous-tâches se terminent :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinTâche
abstract
class
ForkJoinTâche extends
RecursiveAction {
String nom;
ForkJoinTâche
(
) {
this
.nom =
getClass
(
).getSimpleName
(
);
}
ForkJoinTâche
(
int
n) {
this
.nom =
getClass
(
).getSimpleName
(
) +
" "
+
n;
}
ForkJoinTâche
(
String nom) {
this
.nom =
nom;
}
protected
void
compute
(
) {
Logger.println
(
"[%s] Début"
, nom);
calcul
(
);
Logger.println
(
"[%s] Fin"
, nom);
}
abstract
void
calcul
(
);
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinMemeThread
class
SousTâche extends
ForkJoinTâche {
void
calcul
(
) {
TimeUnit.SECONDS.sleep
(
2
);
}
}
class
SousTâche1 extends
SousTâche {}
class
SousTâche2 extends
SousTâche {}
class
Tâche extends
ForkJoinTâche {
void
calcul
(
) {
invokeAll
(
new
SousTâche1
(
), new
SousTâche2
(
));
}
}
new
ForkJoinPool
(
).invoke
(
new
Tâche
(
));
2.
3.
4.
5.
6.
00:00:00.020 [ForkJoinPool-1-worker-1] [Tâche] Début
00:00:00.028 [ForkJoinPool-1-worker-1] [SousTâche1] Début
00:00:00.028 [ForkJoinPool-1-worker-2] [SousTâche2] Début
00:00:02.028 [ForkJoinPool-1-worker-1] [SousTâche1] Fin
00:00:02.028 [ForkJoinPool-1-worker-2] [SousTâche2] Fin
00:00:02.029 [ForkJoinPool-1-worker-1] [Tâche] Fin
On remarque que Tâche et Sous-tâche 1 ont été exécutées par ForkJoinPool-1-worker-1 tandis que Sous-tâche 2 a été exécutée par ForkJoinPool-1-worker-2.
Mais que se passe-t-il s'il n'y a pas assez de threads pour exécuter les sous-tâches ? Ne risque-t-on pas d'avoir un deadlock ?
2.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinManqueThread
new
ForkJoinPool
(
1
).invoke
(
new
Tâche
(
));
2.
3.
4.
5.
6.
[ForkJoinPool-1-worker-1] Début de Tâche
[ForkJoinPool-1-worker-1] Début de Sous-tâche 1
[ForkJoinPool-1-worker-1] Fin de Sous-tâche 1
[ForkJoinPool-1-worker-1] Début de Sous-tâche 2
[ForkJoinPool-1-worker-1] Fin de Sous-tâche 2
[ForkJoinPool-1-worker-1] Fin de Tâche
L'exemple est similaire au précédent, excepté que l'on a défini un niveau de parallélisme à 1. On remarque alors que les deux sous-tâches ont été exécutées par le thread qui était en train d'exécuter la tâche principale.
En revanche, il est possible de saturer votre pool si vous effectuez des tâches bloquantes :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinBloqué
class
Verrou {
void
prendre
(
); // Attend jusqu'à obtention verrou
void
libérer
(
); // Libère le verrou
}
Verrou verrou =
new
Verrou
(
);
class
Bloqueur extends
ForkJoinTâche {
void
calcul
(
) {
Logger.println
(
"[%s] Pose du verrou"
, nom);
verrou.prendre
(
);
Logger.println
(
"[%s] Calcul"
, nom);
TimeUnit.SECONDS.sleep
(
5
);
verrou.libérer
(
);
}
}
class
SousTâche extends
ForkJoinTâche {
SousTâche
(
int
n) {
super
(
n);
}
void
calcul
(
) {
TimeUnit.SECONDS.sleep
(
1
);
}
}
class
Tâche extends
ForkJoinTâche {
protected
void
calcul
(
) {
List<
ForkJoinTask>
actions =
new
ArrayList<>(
);
actions.add
(
new
Bloqueur
(
));
for
(
int
i =
0
; i <
5
; i++
) {
actions.add
(
new
SousTâche
(
i));
}
invokeAll
(
actions);
}
}
Logger.println
(
"Pose du verrou"
);
verrou.prendre
(
);
Logger.println
(
"Soumission"
);
Tâche tâche =
new
Tâche
(
);
new
ForkJoinPool
(
1
).execute
(
tâche);
Logger.println
(
"Pause"
);
TimeUnit.SECONDS.sleep
(
5
);
Logger.println
(
"Libération du verrou"
);
verrou.libérer
(
);
tâche.join
(
);
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
00:00:00.018 [main ] Pose du verrou
00:00:00.026 [main ] Soumission
00:00:00.029 [main ] Pause
00:00:00.029 [ForkJoinPool-1-worker-1] [Tâche] Début
00:00:00.030 [ForkJoinPool-1-worker-1] [Bloqueur] Début
00:00:00.030 [ForkJoinPool-1-worker-1] [Bloqueur] Pose du verrou
00:00:05.029 [main ] Libération du verrou
00:00:05.029 [ForkJoinPool-1-worker-1] [Bloqueur] Calcul
00:00:10.030 [ForkJoinPool-1-worker-1] [Bloqueur] Fin
00:00:10.030 [ForkJoinPool-1-worker-1] [SousTâche 0] Début
00:00:11.031 [ForkJoinPool-1-worker-1] [SousTâche 0] Fin
00:00:11.031 [ForkJoinPool-1-worker-1] [SousTâche 1] Début
00:00:12.032 [ForkJoinPool-1-worker-1] [SousTâche 1] Fin
00:00:12.032 [ForkJoinPool-1-worker-1] [SousTâche 2] Début
00:00:13.033 [ForkJoinPool-1-worker-1] [SousTâche 2] Fin
00:00:13.033 [ForkJoinPool-1-worker-1] [SousTâche 3] Début
00:00:14.034 [ForkJoinPool-1-worker-1] [SousTâche 3] Fin
00:00:14.034 [ForkJoinPool-1-worker-1] [SousTâche 4] Début
00:00:15.036 [ForkJoinPool-1-worker-1] [SousTâche 4] Fin
00:00:15.036 [ForkJoinPool-1-worker-1] [Tâche] Fin
Il est dommage qu'il ait fallu attendre que la tâche bloquante ait accès à la ressource pour exécuter les tâches qui n'en dépendaient pas.
Pour résoudre ce problème, Fork/Join framework propose l'interface ManagedBlocker. L'interface se compose de deux méthodes :
- isReleasable : elle doit renvoyer vrai (true) lorsqu'il n'est pas/plus nécessaire de bloquer. Cela consiste généralement à essayer d'obtenir l'accès à la ressource de manière non bloquante ;
- block : gère l'appel bloquant et renvoie vrai (true) si l'exécution peut se poursuivre.
Une fois l'instance créée, elle doit être passée en paramètre de la méthode statique ForkJoinPool.managedBlock. Retravaillons notre exemple :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinManagedBlocker
class
Verrou {
// ...
boolean
tenter
(
); // Essaye de prendre le verrou et renvoie le résultat.
}
Verrou verrou =
new
Verrou
(
);
class
Verrouilleur implements
ManagedBlocker, AutoCloseable {
boolean
verrouillé =
false
;
Verrouilleur
(
) {
ForkJoinPool.managedBlock
(
this
);
}
public
boolean
isReleasable
(
) {
if
(!
verrouillé) {
verrouillé =
verrou.tenter
(
);
}
return
verrouillé;
}
public
boolean
block
(
) {
if
(!
verrouillé) {
verrou.prendre
(
);
verrouillé =
true
;
}
return
verrouillé;
}
public
void
close
(
) {
verrou.libérer
(
);
verrouillé =
false
;
}
}
class
Bloqueur extends
ForkJoinTâche {
void
calcul
(
) {
Logger.println
(
"[%s] Pose du verrou"
, nom);
try
(
Verrouilleur verrouilleur =
new
Verrouilleur
(
)) {
Logger.println
(
"[%s] Calcul"
, nom);
TimeUnit.SECONDS.sleep
(
2
);
}
}
}
class
SousTâche extends
ForkJoinTâche {
SousTâche
(
int
n) {
super
(
n);
}
void
calcul
(
) {
TimeUnit.SECONDS.sleep
(
1
);
}
}
class
Tâche extends
ForkJoinTâche {
protected
void
calcul
(
) {
List<
ForkJoinTask>
actions =
new
ArrayList<>(
);
actions.add
(
new
Bloqueur
(
));
for
(
int
i =
0
; i <
10
; i++
) {
actions.add
(
new
SousTâche
(
i));
}
invokeAll
(
actions);
}
}
Logger.println
(
"Pose du verrou"
);
verrou.prendre
(
);
Logger.println
(
"Soumission"
);
Tâche tâche =
new
Tâche
(
);
new
ForkJoinPool
(
1
).execute
(
tâche);
Logger.println
(
"Pause"
);
TimeUnit.SECONDS.sleep
(
3
);
Logger.println
(
"Libération du verrou"
);
verrou.libérer
(
);
tâche.join
(
);
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
00:00:00.027 [main ] Pose du verrou
00:00:00.037 [main ] Soumission
00:00:00.040 [main ] Pause
00:00:00.041 [ForkJoinPool-1-worker-1] [Tâche] Début
00:00:00.043 [ForkJoinPool-1-worker-1] [Bloqueur] Début
00:00:00.043 [ForkJoinPool-1-worker-1] [Bloqueur] Pose du verrou
00:00:00.047 [ForkJoinPool-1-worker-0] [SousTâche 9] Début
00:00:01.047 [ForkJoinPool-1-worker-0] [SousTâche 9] Fin
00:00:01.047 [ForkJoinPool-1-worker-0] [SousTâche 8] Début
00:00:02.048 [ForkJoinPool-1-worker-0] [SousTâche 8] Fin
00:00:02.048 [ForkJoinPool-1-worker-0] [SousTâche 7] Début
00:00:03.041 [main ] Libération du verrou
00:00:03.041 [ForkJoinPool-1-worker-1] [Bloqueur] Calcul
00:00:03.049 [ForkJoinPool-1-worker-0] [SousTâche 7] Fin
00:00:03.049 [ForkJoinPool-1-worker-0] [SousTâche 6] Début
00:00:04.050 [ForkJoinPool-1-worker-0] [SousTâche 6] Fin
00:00:04.050 [ForkJoinPool-1-worker-0] [SousTâche 5] Début
00:00:05.043 [ForkJoinPool-1-worker-1] [Bloqueur] Fin
00:00:05.043 [ForkJoinPool-1-worker-1] [SousTâche 0] Début
00:00:05.052 [ForkJoinPool-1-worker-0] [SousTâche 5] Fin
00:00:05.052 [ForkJoinPool-1-worker-0] [SousTâche 4] Début
00:00:06.044 [ForkJoinPool-1-worker-1] [SousTâche 0] Fin
00:00:06.044 [ForkJoinPool-1-worker-1] [SousTâche 1] Début
00:00:06.053 [ForkJoinPool-1-worker-0] [SousTâche 4] Fin
00:00:06.053 [ForkJoinPool-1-worker-0] [SousTâche 3] Début
00:00:07.045 [ForkJoinPool-1-worker-1] [SousTâche 1] Fin
00:00:07.045 [ForkJoinPool-1-worker-1] [SousTâche 2] Début
00:00:07.053 [ForkJoinPool-1-worker-0] [SousTâche 3] Fin
00:00:08.046 [ForkJoinPool-1-worker-1] [SousTâche 2] Fin
00:00:08.046 [ForkJoinPool-1-worker-1] [Tâche] Fin
On remarque alors deux choses. La première, c'est que le pool a alloué un nouveau thread pour traiter les tâches.
La deuxième chose, c'est que les sous-tâches n'ont pas été exécutées selon l'ordre de soumission à cause du (grâce au ?) work-stealing algorithm. Le deuxième thread a pris les tâches selon leur ordre inverse pour limiter l'utilisation de verrou pour accéder à la liste de tâches. En utilisant les deux extrémités de la liste des tâches, on limite les contentions.
Il peut-être intéressant de savoir que Phaser (1.7+) et l'API CompletableFuture (1.8+) exploite ce mécanisme pour limiter les contentions.
Il est important de noter que le comportement avec une JVM 1.7 n'est pas exactement le même. En effet, dans la version précédente, le nouveau thread ne traitait les tâches que le temps du blocage. Comme le montrent les traces ci-dessous.
Cependant, il est important de savoir que l'utilisation de ManagedBlocker est surtout dédiée à limiter les deadlocks. En effet, chaque worker bloqué n'est pas remplacé. Des workers supplémentaires sont uniquement alloués si le pool le permet (limite du nombre de threads) et si TOUS les workers sont bloqués :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinManagedBlockerLimite
class
TâcheBloquante extends
ForkJoinTâche {
Verrou verrou1 =
new
Verrou
(
);
Verrou verrou2 =
new
Verrou
(
);
TâcheBloquante
(
int
n) {
super
(
n);
}
protected
void
calcul
(
) {
Logger.println
(
"[%s] Attente non-managed"
, nom);
verrou1.prendre
(
);
Logger.println
(
"[%s] Passage en mode 'managed'"
, nom);
try
(
Verrouilleur verrouilleur =
new
Verrouilleur
(
verrou2)) {}
}
}
class
SousTâche extends
ForkJoinTâche {
SousTâche
(
int
n) {
super
(
n);
}
@Override
void
calcul
(
) {
TimeUnit.SECONDS.sleep
(
1
);
}
}
class
Tâche extends
ForkJoinTâche {
@Override
void
calcul
(
) {
List<
ForkJoinTâche>
soustâches =
new
ArrayList<>(
);
for
(
int
i =
1
; i <=
5
; i++
) {
soustâches.add
(
new
SousTâche
(
i));
}
invokeAll
(
soustâches);
}
}
int
parallélisme =
2
;
ForkJoinPool pool =
new
ForkJoinPool
(
parallélisme);
Logger.println
(
"Saturation du pool"
);
List<
TâcheBloquante>
bloqués =
new
ArrayList<>(
parallélisme);
for
(
int
i =
1
; i <=
parallélisme; i++
) {
TâcheBloquante bloqué =
new
TâcheBloquante
(
i);
bloqué.verrou1.prendre
(
);
bloqué.verrou2.prendre
(
);
pool.submit
(
bloqué);
bloqués.add
(
bloqué);
}
Logger.println
(
"Ajout de tâches supplémentaires"
);
Tâche tâche =
new
Tâche
(
);
pool.submit
(
tâche);
for
(
TâcheBloquante bloqué : bloqués) {
TimeUnit.SECONDS.sleep
(
5
);
bloqué.verrou1.libérer
(
);
}
tâche.join
(
);
Logger.println
(
"Libération des tâches bloquantes"
);
for
(
TâcheBloquante bloqué : bloqués) {
bloqué.verrou2.libérer
(
);
bloqué.join
(
);
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
00:00:00.020 [main ] Saturation du pool
00:00:00.031 [main ] Ajout de tâches supplémentaires
00:00:00.031 [ForkJoinPool-1-worker-0] [TâcheBloquante 2] Début
00:00:00.031 [ForkJoinPool-1-worker-0] [TâcheBloquante 2] Attente non-managed
00:00:00.031 [ForkJoinPool-1-worker-1] [TâcheBloquante 1] Début
00:00:00.032 [ForkJoinPool-1-worker-1] [TâcheBloquante 1] Attente non-managed
00:00:05.032 [ForkJoinPool-1-worker-1] [TâcheBloquante 1] Passage en mode 'managed'
00:00:10.032 [ForkJoinPool-1-worker-0] [TâcheBloquante 2] Passage en mode 'managed'
00:00:10.033 [ForkJoinPool-1-worker-2] [Tâche] Début
00:00:10.034 [ForkJoinPool-1-worker-2] [SousTâche 1] Début
00:00:11.035 [ForkJoinPool-1-worker-2] [SousTâche 1] Fin
00:00:11.035 [ForkJoinPool-1-worker-2] [SousTâche 2] Début
00:00:12.036 [ForkJoinPool-1-worker-2] [SousTâche 2] Fin
00:00:12.036 [ForkJoinPool-1-worker-2] [SousTâche 3] Début
00:00:13.037 [ForkJoinPool-1-worker-2] [SousTâche 3] Fin
00:00:13.037 [ForkJoinPool-1-worker-2] [SousTâche 4] Début
00:00:14.038 [ForkJoinPool-1-worker-2] [SousTâche 4] Fin
00:00:14.038 [ForkJoinPool-1-worker-2] [SousTâche 5] Début
00:00:15.039 [ForkJoinPool-1-worker-2] [SousTâche 5] Fin
00:00:15.039 [ForkJoinPool-1-worker-2] [Tâche] Fin
00:00:15.040 [main ] Libération des tâches bloquantes
00:00:15.040 [ForkJoinPool-1-worker-1] [TâcheBloquante 1] Fin
00:00:15.041 [ForkJoinPool-1-worker-0] [TâcheBloquante 2] Fin
On a commencé par saturer le pool avec des tâches bloquantes, puis on a ajouté une tâche qui produisait cinq sous-tâches. On constate alors que rien ne se produit.
Ensuite, on passe peu à peu les tâches bloquantes en mode « managed ». Tant qu'elles ne sont pas toutes passées en mode « managed », il ne se passe toujours rien.
Dès que la dernière bloquante est passée en mode « managed », le pool alloue uniquement un troisième worker qui traite la tâche (et toutes ses filles).
Finalement on libère les tâches bloquantes.
Si vous devez utiliser des opérations bloquantes (verrous ou non), il est recommandé de s'orienter vers des API asynchrones.
IV. Conclusion▲
Au cours de ce premier article, nous avons vu comment sont représentés les threads en Java et comment interagir avec eux. Ceci pose les fondations de la programmation parallèle, mais les prochains articles seront entièrement dédiés à gérer la concurrence dans votre code à l'aide de l'API native (intégrée au langage) et des outils fournis par Java SE.
V. Remerciements▲
Je remercie tous les contributeurs de l'API Java et les packages java.util.concurrent.* et plus particulièrement Doug Lea qui est l'un des principaux auteurs.
Je remercie également Mickael Baron, Thierry Leriche-Dessirier alias thierryler et Claude Leloup pour leur relecture attentive, leurs remarques et leurs bons conseils.
Je tiens aussi à remercier la communauté Developpez.com qui a mis en place tous les outils, procédures et l'hébergement nécessaires à la publication de cet article.
Enfin mon épouse et mes enfants pour leur patience et leur tolérance durant les nombreuses heures qui ont été nécessaires à la rédaction de cet article.
VI. Annexes▲
VI-A. Sources des exemples▲
Tous les exemples donnés dans cet article sont disponibles sous la forme d'un projet Maven hébergé sous GitHub. Tous les exemples cités contiennent une première ligne commentaire indiquant l'emplacement du fichier dans les sources. Les sources propres à ce chapitre se trouve sous le package com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.
Si vous ne savez pas comment importer le projet, je vous invite à consulter l'article « Importer un projet Maven dans Eclipse en 5 minutes ».
VI-B. Java Concurrent Animated▲
Java Concurrent Animated est un projet Swing visant à montrer graphiquement le comportement de différents composants de l'API concurrente de Java.