Java-Tipp: Wann sollte ForkJoinPool vs ExecutorService verwendet werden?

Die in Java 7 eingeführte Fork / Join-Bibliothek erweitert das vorhandene Java-Parallelitätspaket um Unterstützung für Hardware-Parallelität, ein Schlüsselmerkmal von Multicore-Systemen. In diesem Java-Tipp demonstriert Madalin Ilie die Auswirkungen des Ersetzens der Java 6- ExecutorServiceKlasse durch Java 7 ForkJoinPoolin einer Webcrawler-Anwendung auf die Leistung.

Webcrawler, auch als Webspider bekannt, sind der Schlüssel zum Erfolg von Suchmaschinen. Diese Programme scannen ständig das Web, sammeln Millionen von Datenseiten und senden sie an Suchmaschinendatenbanken zurück. Die Daten werden dann indiziert und algorithmisch verarbeitet, was zu schnelleren und genaueren Suchergebnissen führt. Während sie am bekanntesten für die Suchoptimierung verwendet werden, können Webcrawler auch für automatisierte Aufgaben wie die Validierung von Links oder das Suchen und Zurückgeben bestimmter Daten (z. B. E-Mail-Adressen) in einer Sammlung von Webseiten verwendet werden.

Architektonisch sind die meisten Webcrawler leistungsstarke Multithread-Programme, wenn auch mit relativ einfachen Funktionen und Anforderungen. Das Erstellen eines Webcrawlers ist daher eine interessante Methode zum Üben sowie zum Vergleichen, Multithreading oder gleichzeitigen Programmieren von Techniken.

Die Rückkehr von Java Tips!

Java-Tipps sind kurze, codegesteuerte Artikel, die JavaWorld-Leser dazu einladen, ihre Programmierkenntnisse und Entdeckungen auszutauschen. Lassen Sie uns wissen, ob Sie einen Tipp für die JavaWorld-Community haben. Weitere Programmiertipps Ihrer Kollegen finden Sie auch im Java Tips Archive.

In diesem Artikel werde ich zwei Ansätze zum Schreiben eines Webcrawlers erläutern: einen mit Java 6 ExecutorService und den anderen mit ForkJoinPool von Java 7. Um den Beispielen folgen zu können, müssen Sie (zum Zeitpunkt dieses Schreibens) Java 7 Update 2 in Ihrer Entwicklungsumgebung sowie die Drittanbieter-Bibliothek HtmlParser installiert haben.

Zwei Ansätze zur gleichzeitigen Verwendung von Java

Die ExecutorServiceKlasse ist Teil der java.util.concurrentRevolution, die in Java 5 (und natürlich in Java 6) eingeführt wurde und die Thread-Behandlung auf der Java-Plattform vereinfachte. ExecutorServiceist ein Executor, der Methoden zum Verwalten der Fortschrittsverfolgung und Beendigung asynchroner Aufgaben bereitstellt. Vor der Einführung von java.util.concurrenthaben sich Java-Entwickler auf Bibliotheken von Drittanbietern verlassen oder eigene Klassen geschrieben, um die Parallelität in ihren Programmen zu verwalten.

Fork / Join, eingeführt in Java 7, soll die vorhandenen Dienstprogrammklassen für Parallelität nicht ersetzen oder mit ihnen konkurrieren. stattdessen werden sie aktualisiert und vervollständigt. Fork / Join befasst sich mit der Notwendigkeit des Teilens und Eroberens oder der rekursiven Aufgabenverarbeitung in Java-Programmen (siehe Ressourcen).

Die Logik von Fork / Join ist sehr einfach: (1) Trennen Sie jede große Aufgabe in kleinere Aufgaben; (2) jede Aufgabe in einem separaten Thread verarbeiten (diese bei Bedarf in noch kleinere Aufgaben aufteilen); (3) verbinden Sie die Ergebnisse.

Die beiden folgenden Webcrawler-Implementierungen sind einfache Programme, die die Merkmale und Funktionen von Java 6 ExecutorServiceund Java 7 demonstrieren ForkJoinPool.

Erstellen und Benchmarking des Webcrawlers

Die Aufgabe unseres Webcrawlers besteht darin, Links zu finden und ihnen zu folgen. Sein Zweck könnte die Linkvalidierung sein oder das Sammeln von Daten. (Sie können das Programm beispielsweise anweisen, im Internet nach Bildern von Angelina Jolie oder Brad Pitt zu suchen.)

Die Anwendungsarchitektur besteht aus folgenden Elementen:

  1. Eine Schnittstelle, die grundlegende Operationen für die Interaktion mit Links verfügbar macht. Ermitteln Sie die Anzahl der besuchten Links, fügen Sie neue Links hinzu, die in der Warteschlange besucht werden sollen, und markieren Sie einen Link als besucht
  2. Eine Implementierung für diese Schnittstelle, die auch der Ausgangspunkt der Anwendung sein wird
  3. Eine Thread- / rekursive Aktion, die die Geschäftslogik enthält, um zu überprüfen, ob ein Link bereits besucht wurde. Wenn nicht, werden alle Links auf der entsprechenden Seite gesammelt, ein neuer Thread / eine neue rekursive Aufgabe erstellt und an das ExecutorServiceoder gesendetForkJoinPool
  4. Ein ExecutorServiceoder ForkJoinPoolum wartende Aufgaben zu erledigen

Beachten Sie, dass ein Link als "besucht" betrachtet wird, nachdem alle Links auf der entsprechenden Seite zurückgegeben wurden.

Zusätzlich zum Vergleich der einfachen Entwicklung mit den in Java 6 und Java 7 verfügbaren Parallelitätstools vergleichen wir die Anwendungsleistung anhand von zwei Benchmarks:

  • Suchabdeckung : Misst die Zeit, die erforderlich ist, um 1.500 verschiedene Links zu besuchen
  • Verarbeitungsleistung : Misst die Zeit in Sekunden, die erforderlich ist, um 3.000 nicht eindeutige Links zu besuchen . Dies ist wie das Messen, wie viele Kilobit pro Sekunde Ihre Internetverbindung verarbeitet.

Diese Benchmarks sind zwar relativ einfach, bieten jedoch zumindest einen kleinen Einblick in die Leistung der Java-Parallelität in Java 6 im Vergleich zu Java 7 für bestimmte Anwendungsanforderungen.

Ein mit ExecutorService erstellter Java 6-Webcrawler

Für die Java 6-Webcrawler-Implementierung verwenden wir einen Pool mit 64 Threads mit festem Thread, den wir durch Aufrufen der Executors.newFixedThreadPool(int)Factory-Methode erstellen . Listing 1 zeigt die Implementierung der Hauptklasse.

Listing 1. Erstellen eines WebCrawlers

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(); } }

Im obigen WebCrawler6Konstruktor erstellen wir einen Thread-Pool mit fester Größe von 64 Threads. Wir starten das Programm dann, indem wir die startCrawlingMethode aufrufen , die den ersten Thread erstellt und an die sendet ExecutorService.

Als Nächstes erstellen wir eine LinkHandlerSchnittstelle, die Hilfsmethoden für die Interaktion mit URLs bereitstellt. Die Anforderungen lauten wie folgt: (1) Markieren Sie eine URL mit der addVisited()Methode als besucht . (2) Ermitteln der Nummer der besuchten URLs über die size()Methode; (3) festzustellen, ob eine URL bereits mit der visited()Methode besucht wurde; und (4) Hinzufügen einer neuen URL in die Warteschlange über die queueLink()Methode.

Listing 2. Die LinkHandler-Schnittstelle

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); }

Jetzt müssen wir beim Crawlen von Seiten den Rest der Threads starten, was wir über die LinkFinderSchnittstelle tun , wie in Listing 3 gezeigt. Beachten Sie die linkHandler.queueLink(l)Zeile.

Listing 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 } } } }

Die Logik von LinkFinderist einfach: (1) Wir beginnen mit dem Parsen einer URL. (2) Nachdem wir alle Links auf der entsprechenden Seite gesammelt haben, markieren wir die Seite als besucht. und (3) wir senden jeden gefundenen Link an eine Warteschlange, indem wir die queueLink()Methode aufrufen . Diese Methode erstellt tatsächlich einen neuen Thread und sendet ihn an die ExecutorService. Wenn "freie" Threads im Pool verfügbar sind, wird der Thread ausgeführt. Andernfalls wird es in eine Warteschlange gestellt. Nachdem wir 1.500 verschiedene besuchte Links erreicht haben, drucken wir die Statistiken und das Programm läuft weiter.

Ein Java 7-Webcrawler mit ForkJoinPool

Das in Java 7 eingeführte Fork / Join-Framework ist eine Implementierung des Divide and Conquer-Algorithmus (siehe Ressourcen), bei dem eine Zentrale ForkJoinPoolVerzweigungen ausführt ForkJoinTask. In diesem Beispiel verwenden wir ein ForkJoinPool"Backed" von 64 Threads. Ich sage unterstützt, weil ForkJoinTasks leichter als Fäden sind. In Fork / Join kann eine große Anzahl von Aufgaben von einer kleineren Anzahl von Threads gehostet werden.

Ähnlich wie bei der Java 6-Implementierung instanziieren wir zunächst im WebCrawler7Konstruktor ein ForkJoinPoolObjekt, das von 64 Threads unterstützt wird.

Listing 4. Java 7 LinkHandler-Implementierung

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(); } }

Beachten Sie, dass die LinkHandlerSchnittstelle in Listing 4 fast mit der Java 6-Implementierung aus Listing 2 identisch ist. Es fehlt nur die queueLink()Methode. Die wichtigsten Methoden sind der Konstruktor und die startCrawling()Methode. Im Konstruktor erstellen wir einen neuen ForkJoinPool, von 64 Threads unterstützten. (Ich habe 64 Threads anstelle von 50 oder einer anderen runden Zahl ausgewählt, da im ForkJoinPoolJavadoc angegeben ist, dass die Anzahl der Threads eine Zweierpotenz sein muss.) Der Pool ruft eine neue auf LinkFinderAction, die rekursiv weiter aufgerufen wird ForkJoinTasks. Listing 5 zeigt die LinkFinderActionKlasse: