Astuce Java: Quand utiliser ForkJoinPool vs ExecutorService

La bibliothèque Fork / Join introduite dans Java 7 étend le package d'accès concurrentiel Java existant avec la prise en charge du parallélisme matériel, une caractéristique clé des systèmes multicœurs. Dans cette astuce Java, Madalin Ilie démontre l'impact sur les performances du remplacement de la ExecutorServiceclasse Java 6 par Java 7 ForkJoinPooldans une application de robot d'exploration Web.

Les robots d'exploration Web, également connus sous le nom d'araignées Web, sont la clé du succès des moteurs de recherche. Ces programmes analysent en permanence le Web, rassemblant des millions de pages de données et les renvoyant aux bases de données des moteurs de recherche. Les données sont ensuite indexées et traitées de manière algorithmique, ce qui permet d'obtenir des résultats de recherche plus rapides et plus précis. Bien qu'ils soient surtout utilisés pour l'optimisation de la recherche, les robots d'exploration Web peuvent également être utilisés pour des tâches automatisées telles que la validation de lien ou la recherche et le renvoi de données spécifiques (telles que des adresses e-mail) dans une collection de pages Web.

Sur le plan architectural, la plupart des robots d'exploration Web sont des programmes multithread hautes performances, bien qu'avec des fonctionnalités et des exigences relativement simples. Construire un robot d'exploration Web est donc une manière intéressante de s'entraîner, ainsi que de comparer des techniques de programmation multithread ou concurrentes.

Le retour des conseils Java!

Les conseils Java sont de courts articles basés sur le code qui invitent les lecteurs de JavaWorld à partager leurs compétences en programmation et leurs découvertes. Faites-nous savoir si vous avez une astuce à partager avec la communauté JavaWorld. Consultez également l'archive des conseils Java pour obtenir plus de conseils de programmation de vos pairs.

Dans cet article, je vais vous présenter deux approches pour écrire un robot d'exploration Web: l'une utilisant Java 6 ExecutorService et l'autre ForkJoinPool de Java 7. Afin de suivre les exemples, vous devez avoir (au moment d'écrire ces lignes) Java 7 update 2 installé dans votre environnement de développement, ainsi que la bibliothèque tierce HtmlParser.

Deux approches de la concurrence Java

La ExecutorServiceclasse fait partie de la java.util.concurrentrévolution introduite dans Java 5 (et partie de Java 6, bien sûr), qui a simplifié la gestion des threads sur la plate-forme Java. ExecutorServiceest un exécuteur qui fournit des méthodes pour gérer le suivi de la progression et l'arrêt des tâches asynchrones. Avant l'introduction de java.util.concurrent, les développeurs Java s'appuyaient sur des bibliothèques tierces ou écrivaient leurs propres classes pour gérer la concurrence dans leurs programmes.

Fork / Join, introduit dans Java 7, n'est pas destiné à remplacer ou concurrencer les classes d'utilitaires d'accès concurrentiel existantes; au lieu de cela, il les met à jour et les complète. Fork / Join répond au besoin de diviser et conquérir, ou traitement de tâches récursif dans les programmes Java (voir Ressources).

La logique de Fork / Join est très simple: (1) séparer (fork) chaque grande tâche en tâches plus petites; (2) traiter chaque tâche dans un thread distinct (en les séparant en tâches encore plus petites si nécessaire); (3) rejoignez les résultats.

Les deux implémentations de robot d'exploration Web qui suivent sont des programmes simples qui illustrent les caractéristiques et les fonctionnalités de Java 6 ExecutorServiceet de Java 7 ForkJoinPool.

Création et analyse comparative du robot d'exploration Web

La tâche de notre robot d'exploration sera de trouver et de suivre des liens. Son objectif peut être la validation des liens ou la collecte de données. (Vous pouvez, par exemple, demander au programme de rechercher sur le Web des photos d'Angelina Jolie ou de Brad Pitt.)

L'architecture de l'application comprend les éléments suivants:

  1. Une interface qui expose les opérations de base pour interagir avec les liens; c'est-à-dire obtenir le nombre de liens visités, ajouter de nouveaux liens à visiter dans la file d'attente, marquer un lien comme visité
  2. Une implémentation de cette interface qui sera également le point de départ de l'application
  3. Un thread / action récursive qui contiendra la logique métier pour vérifier si un lien a déjà été visité. Sinon, il rassemblera tous les liens de la page correspondante, créera un nouveau fil / tâche récursive et le soumettra au ExecutorServiceouForkJoinPool
  4. Une ExecutorServiceou ForkJoinPoolpour gérer les tâches en attente

Notez qu'un lien est considéré comme «visité» après que tous les liens de la page correspondante ont été renvoyés.

En plus de comparer la facilité de développement à l'aide des outils de concurrence disponibles dans Java 6 et Java 7, nous comparerons les performances des applications en fonction de deux benchmarks:

  • Couverture de la recherche : mesure le temps nécessaire pour visiter 1500 liens distincts
  • Puissance de traitement : mesure le temps en secondes requis pour visiter 3 000 liaisons non distinctes ; c'est comme mesurer le nombre de kilobits par seconde que votre connexion Internet traite.

Bien que relativement simples, ces tests fourniront au moins une petite fenêtre sur les performances de la concurrence Java dans Java 6 par rapport à Java 7 pour certaines exigences d'application.

Un robot d'exploration Web Java 6 construit avec ExecutorService

Pour l'implémentation du robot d'exploration Web Java 6, nous utiliserons un pool de threads fixes de 64 threads, que nous créons en appelant la Executors.newFixedThreadPool(int)méthode factory. Le listing 1 montre l'implémentation de la classe principale.

Listing 1. Construire un WebCrawler

package insidecoding.webcrawler; import java.util.Collection; import java.util.Collections; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import insidecoding.webcrawler.net.LinkFinder; import java.util.HashSet; /** * * @author Madalin Ilie */ public class WebCrawler6 implements LinkHandler { private final Collection visitedLinks = Collections.synchronizedSet(new HashSet()); // private final Collection visitedLinks = Collections.synchronizedList(new ArrayList()); private String url; private ExecutorService execService; public WebCrawler6(String startingURL, int maxThreads) { this.url = startingURL; execService = Executors.newFixedThreadPool(maxThreads); } @Override public void queueLink(String link) throws Exception { startNewThread(link); } @Override public int size() { return visitedLinks.size(); } @Override public void addVisited(String s) { visitedLinks.add(s); } @Override public boolean visited(String s) { return visitedLinks.contains(s); } private void startNewThread(String link) throws Exception { execService.execute(new LinkFinder(link, this)); } private void startCrawling() throws Exception { startNewThread(this.url); } /** * @param args the command line arguments */ public static void main(String[] args) throws Exception { new WebCrawler("//www.javaworld.com", 64).startCrawling(); } }

Dans le WebCrawler6constructeur ci-dessus , nous créons un pool de threads de taille fixe de 64 threads. Nous démarrons ensuite le programme en appelant la startCrawlingméthode, qui crée le premier thread et le soumet au ExecutorService.

Ensuite, nous créons une LinkHandlerinterface qui expose des méthodes d'assistance pour interagir avec les URL. Les exigences sont les suivantes: (1) marquer une URL comme visitée à l'aide de la addVisited()méthode; (2) obtenir le nombre d'URL visitées via la size()méthode; (3) déterminer si une URL a déjà été visitée en utilisant la visited()méthode; et (4) ajouter une nouvelle URL dans la file d'attente via la queueLink()méthode.

Listing 2. L'interface LinkHandler

package insidecoding.webcrawler; /** * * @author Madalin Ilie */ public interface LinkHandler { /** * Places the link in the queue * @param link * @throws Exception */ void queueLink(String link) throws Exception; /** * Returns the number of visited links * @return */ int size(); /** * Checks if the link was already visited * @param link * @return */ boolean visited(String link); /** * Marks this link as visited * @param link */ void addVisited(String link); }

Maintenant, pendant que nous explorons les pages, nous devons démarrer le reste des threads, ce que nous faisons via l' LinkFinderinterface, comme indiqué dans le Listing 3. Notez la linkHandler.queueLink(l)ligne.

Liste 3. LinkFinder

package insidecoding.webcrawler.net; import java.net.URL; import org.htmlparser.Parser; import org.htmlparser.filters.NodeClassFilter; import org.htmlparser.tags.LinkTag; import org.htmlparser.util.NodeList; import insidecoding.webcrawler.LinkHandler; /** * * @author Madalin Ilie */ public class LinkFinder implements Runnable { private String url; private LinkHandler linkHandler; /** * Used fot statistics */ private static final long t0 = System.nanoTime(); public LinkFinder(String url, LinkHandler handler) { this.url = url; this.linkHandler = handler; } @Override public void run() { getSimpleLinks(url); } private void getSimpleLinks(String url) { //if not already visited if (!linkHandler.visited(url)) { try { URL uriLink = new URL(url); Parser parser = new Parser(uriLink.openConnection()); NodeList list = parser.extractAllNodesThatMatch(new NodeClassFilter(LinkTag.class)); List urls = new ArrayList(); for (int i = 0; i < list.size(); i++) { LinkTag extracted = (LinkTag) list.elementAt(i); if (!extracted.getLink().isEmpty() && !linkHandler.visited(extracted.getLink())) { urls.add(extracted.getLink()); } } //we visited this url linkHandler.addVisited(url); if (linkHandler.size() == 1500) { System.out.println("Time to visit 1500 distinct links = " + (System.nanoTime() - t0)); } for (String l : urls) { linkHandler.queueLink(l); } } catch (Exception e) { //ignore all errors for now } } } }

La logique du LinkFinderest simple: (1) nous commençons à analyser une URL; (2) après avoir rassemblé tous les liens dans la page correspondante, nous marquons la page comme visitée; et (3) nous envoyons chaque lien trouvé vers une file d'attente en appelant la queueLink()méthode. Cette méthode crée en fait un nouveau thread et l'envoie au ExecutorService. Si des threads "libres" sont disponibles dans le pool, le thread sera exécuté; sinon, il sera placé dans une file d'attente. Après avoir atteint 1 500 liens distincts visités, nous imprimons les statistiques et le programme continue de fonctionner.

Un robot d'exploration Java 7 avec ForkJoinPool

Le framework Fork / Join introduit dans Java 7 est en fait une implémentation de l'algorithme Divide and Conquer (voir Ressources), dans lequel une centrale ForkJoinPoolexécute des branchements ForkJoinTask. Pour cet exemple, nous utiliserons un ForkJoinPool"soutenu" par 64 threads. Je dis soutenu car les ForkJoinTasks sont plus légers que les fils. Dans Fork / Join, un grand nombre de tâches peuvent être hébergées par un plus petit nombre de threads.

Similaire à l'implémentation Java 6, nous commençons par instancier dans le WebCrawler7constructeur un ForkJoinPoolobjet soutenu par 64 threads.

Listing 4. Implémentation de Java 7 LinkHandler

package insidecoding.webcrawler7; import java.util.Collection; import java.util.Collections; import java.util.concurrent.ForkJoinPool; import insidecoding.webcrawler7.net.LinkFinderAction; import java.util.HashSet; /** * * @author Madalin Ilie */ public class WebCrawler7 implements LinkHandler { private final Collection visitedLinks = Collections.synchronizedSet(new HashSet()); // private final Collection visitedLinks = Collections.synchronizedList(new ArrayList()); private String url; private ForkJoinPool mainPool; public WebCrawler7(String startingURL, int maxThreads) { this.url = startingURL; mainPool = new ForkJoinPool(maxThreads); } private void startCrawling() { mainPool.invoke(new LinkFinderAction(this.url, this)); } @Override public int size() { return visitedLinks.size(); } @Override public void addVisited(String s) { visitedLinks.add(s); } @Override public boolean visited(String s) { return visitedLinks.contains(s); } /** * @param args the command line arguments */ public static void main(String[] args) throws Exception { new WebCrawler7("//www.javaworld.com", 64).startCrawling(); } }

Notez que l' LinkHandlerinterface du Listing 4 est presque la même que l'implémentation Java 6 du Listing 2. Il ne manque que la queueLink()méthode. Les méthodes les plus importantes à examiner sont le constructeur et la startCrawling()méthode. Dans le constructeur, nous créons un nouveau ForkJoinPoolsupporté par 64 threads. (J'ai choisi 64 threads au lieu de 50 ou un autre nombre rond car dans le ForkJoinPoolJavadoc, il indique que le nombre de threads doit être une puissance de deux.) Le pool invoque un nouveau LinkFinderAction, qui invoquera de manière récursive plus loin ForkJoinTasks. Le listing 5 montre la LinkFinderActionclasse: