Contenu du cours
Multithreading en Java
Multithreading en Java
ForkJoinPool
La classe ForkJoinPool
en Java pour travailler avec le framework Fork/Join
est justement la réalisation de cela. Elle fournit des mécanismes pour gérer des tâches qui peuvent être divisées en sous-tâches plus petites et exécutées en parallèle.
Main
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinPool
est basé sur le concept de deux actions de base : fork et join. Fork
est une action où nous décomposons une grande tâche en plusieurs sous-tâches plus petites qui peuvent être exécutées en parallèle. Join
est le processus par lequel les résultats de ces sous-tâches sont combinés pour former le résultat de la tâche originale.
Étude de Cas
Imaginez que vous devez analyser un ensemble de données énorme qui peut être divisé en plusieurs ensembles plus petits. Si vous traitez chaque ensemble de données séparément puis fusionnez les résultats, vous pouvez accélérer le traitement de manière significative, surtout sur des systèmes multiprocesseurs.
Comment utiliser ForkJoinPool
Il y a 2 classes pour implémenter des tâches en elles, ce sont RecursiveTask
et RecursiveAction
. Les deux nécessitent qu'une méthode abstraite compute()
soit implémentée. Dans RecursiveTask
, la méthode compute()
retourne une valeur, tandis que dans RecursiveAction
, la méthode compute()
retourne void.
Main
class ExampleTask extends RecursiveTask<String> { @Override protected String compute() { // code return null; } }
Lorsque nous héritons d'un RecursiveTask
, nous devons nous assurer de spécifier quel type de données notre méthode compute()
va retourner en utilisant cette syntaxe RecursiveTask<String>
.
Main
class ExampleAction extends RecursiveAction { @Override protected void compute() { // code } }
Ici, c'est l'inverse, nous n'avons pas besoin de spécifier le type explicitement dans RecursiveAction
, car notre méthode ne retournera rien et effectuera simplement la tâche.
Démarrer une Tâche
D'ailleurs, nous pouvons démarrer une tâche pour exécution sans même utiliser ForkJoinPool
, simplement en utilisant les méthodes fork()
et join()
.
Il est important de noter que la méthode fork()
envoie la tâche à un certain thread. La méthode join()
est utilisée pour obtenir le résultat.
Main
public static void main(String[] args) { ExampleTask simpleClass = new ExampleTask(); simpleClass.fork(); System.out.println(simpleClass.join()); }
Méthodes principales de ForkJoinPool
invoke()
: Démarre une tâche dans le pool et attend qu'elle soit terminée. Retourne le résultat de l'achèvement de la tâche;submit()
: Soumet une tâche au pool, mais ne bloque pas le thread actuel en attendant que la tâche soit terminée. Peut être utilisé pour soumettre des tâches et récupérer leurs objetsFuture
;execute()
: Exécute une tâche dans le pool, mais ne retourne pas de résultat et ne bloque pas le thread actuel.
Comment pouvons-nous démarrer une tâche en utilisant ForkJoinPool
, très simple !
Main
public static void main(String[] args) { ExampleAction simpleClass = new ExampleAction(); ForkJoinPool forkJoinPool = new ForkJoinPool(); System.out.println(forkJoinPool.invoke(simpleClass)); }
Mais quelle est la différence entre l'exécution de fork()
, join()
et via la classe ForkJoinPool
?
C'est très simple ! Lors de l'utilisation de la première méthode avec l'aide des méthodes fork()
, join()
, nous démarrons la tâche dans le même thread dans lequel ces méthodes ont été appelées, bloquant ce thread, tandis qu'avec l'aide de la classe ForkJoinPool
, nous sommes alloués à un thread du pool et il fonctionne dans ce thread sans bloquer le thread principal.
Examinons de plus près, supposons que nous ayons une telle implémentation :
Main
class ExampleRecursiveTask extends RecursiveTask<String> { @Override protected String compute() { System.out.println("Thread: " + Thread.currentThread().getName()); return "Wow, it works!!!"; } }
Et nous voulons exécuter ce code en utilisant fork()
et join()
. Voyons ce qui sera imprimé dans la console et quel thread effectuera cette tâche.
Main
public class Main { public static void main(String[] args) { ExampleRecursiveTask simpleClass = new ExampleRecursiveTask(); simpleClass.fork(); System.out.println(simpleClass.join()); } }
Et nous obtenons cette sortie sur la console :
Voyons maintenant ce qui se passe si nous exécutons avec ForkJoinPool
:
Main
public class Main { public static void main(String[] args) { ExampleRecursiveTask simpleClass = new ExampleRecursiveTask(); ForkJoinPool forkJoinPool = new ForkJoinPool(); System.out.println(forkJoinPool.invoke(simpleClass)); } }
Et nous arrivons à cette conclusion :
Et comme vous pouvez le voir, la différence est qu'en utilisant la 1ère méthode, la tâche est exécutée dans le même thread qui a appelé cette tâche (thread principal). Mais si nous utilisons la 2ème méthode, le thread est pris dans le pool de threads ForkJoinPool
et le thread principal qui a appelé cette logique n'est pas bloqué et continue !
Comment implémenter Fork/Join dans le code
La façon la plus simple d'expliquer cela serait dans une vidéo, plutôt que de vous donner 50-80 lignes de code et de l'expliquer point par point.
Résumé
ForkJoinPool
est efficace pour les tâches qui peuvent être facilement divisées en sous-tâches plus petites. Cependant, si les tâches sont trop petites, l'utilisation deForkJoinPool
peut ne pas apporter un gain de performance significatif.
Merci pour vos commentaires !