Java-tips: När ska jag använda ForkJoinPool vs ExecutorService

Fork / Join-biblioteket som introducerades i Java 7 utökar det befintliga Java-samtidighetspaketet med stöd för hårdvaruparallellism, en nyckelfunktion i flerkärnsystem. I det här Java-tipet visar Madalin Ilie prestandakonsekvensen av att ersätta Java 6- ExecutorServiceklassen med Java 7 ForkJoinPooli en webb-sökrobot.

Webbsökare, även kända som webbspindlar, är nyckeln till sökmotorernas framgång. Dessa program skannar ständigt webben, samlar in miljontals sidor med data och skickar tillbaka dem till sökmotordatabaser. Uppgifterna indexeras och bearbetas sedan algoritmiskt, vilket resulterar i snabbare och mer exakta sökresultat. Även om de mest kända används för sökoptimering, kan webbsökare också användas för automatiserade uppgifter som länkvalidering eller att hitta och returnera specifika data (t.ex. e-postadresser) i en samling webbsidor.

Arkitektoniskt är de flesta webbsökare högpresterande flertrådade program, om än med relativt enkla funktioner och krav. Att bygga en webbsökare är därför ett intressant sätt att träna, liksom jämföra, flertrådade eller samtidiga programmeringstekniker.

Återkomsten av Java Tips!

Java Tips är korta koddrivna artiklar som bjuder in JavaWorld-läsare att dela med sig av sina programmeringsfärdigheter och upptäckter. Låt oss veta om du har ett tips att dela med JavaWorld-communityn. Kolla även in Java Tips Archive för fler programmeringstips från dina kamrater.

I den här artikeln går jag igenom två tillvägagångssätt för att skriva en webbsökare: en använder Java 6 ExecutorService och den andra Java 7: s ForkJoinPool. För att följa exemplen måste du (från och med detta skrivande) ha Java 7-uppdatering 2 installerad i din utvecklingsmiljö, liksom tredjepartsbiblioteket HtmlParser.

Två tillvägagångssätt för Java-samtidighet

Den ExecutorServiceklass är en del av java.util.concurrentrevolutionen infördes i Java 5 (och en del av Java 6, naturligtvis), som förenklat trådhantering på Java-plattformen. ExecutorServiceär en Executor som tillhandahåller metoder för att hantera framstegsspårning och avslutning av asynkrona uppgifter. Innan introduktionen av java.util.concurrent, Java-utvecklare förlitade sig på tredjepartsbibliotek eller skrev sina egna klasser för att hantera samtidighet i sina program.

Fork / Join, introducerat i Java 7, är inte avsett att ersätta eller konkurrera med de befintliga klasserna för samtidiga verktyg. istället uppdateras och kompletteras. Fork / Join adresserar behovet av delning och erövring eller rekursiv uppgiftsbehandling i Java-program (se Resurser).

Fork / Join logik är väldigt enkel: (1) separera (gaffel) varje stor uppgift i mindre uppgifter; (2) behandla varje uppgift i en separat tråd (separera dem till ännu mindre uppgifter om det behövs); (3) gå med i resultaten.

De två webbcrawlerimplementeringarna som följer är enkla program som visar funktionerna och funktionerna i Java 6 ExecutorServiceoch Java 7 ForkJoinPool.

Bygga och benchmarking av webbsökaren

Vår webbcrawlers uppgift är att hitta och följa länkar. Syftet kan vara länkvalidering eller samla in data. (Du kan till exempel instruera programmet att söka på nätet efter bilder av Angelina Jolie eller Brad Pitt.)

Applikationsarkitekturen består av följande:

  1. Ett gränssnitt som exponerar grundläggande operationer för att interagera med länkar; dvs få antalet besökta länkar, lägg till nya länkar som ska besökas i kö, markera en länk som besökt
  2. En implementering för detta gränssnitt som också kommer att vara startpunkten för applikationen
  3. En tråd / rekursiv åtgärd som håller affärslogiken för att kontrollera om en länk redan har besökts. Om inte, samlar den alla länkar på motsvarande sida, skapar en ny tråd / rekursiv uppgift och skickar den till ExecutorServiceellerForkJoinPool
  4. En ExecutorServiceeller för ForkJoinPoolatt hantera väntande uppgifter

Observera att en länk anses vara "besökt" efter att alla länkar på motsvarande sida har returnerats.

Förutom att jämföra utvecklingen med hjälp av de samtidiga verktyg som finns tillgängliga i Java 6 och Java 7, jämför vi applikationsprestanda baserat på två riktmärken:

  • Sök täckning : Mäter den tid som krävs för att besöka 1 500 olika länkar
  • Processorkraft : Mäter tiden i sekunder som krävs för att besöka 3000 icke-distinkta länkar; det här är som att mäta hur många kilobit per sekund din internetanslutning bearbetar.

Medan de är relativt enkla, kommer dessa riktmärken att ge åtminstone ett litet fönster till prestanda för Java-samtidighet i Java 6 jämfört med Java 7 för vissa applikationskrav.

En Java 6-webbsökare byggd med ExecutorService

För implementeringen av Java 6-webbsökaren använder vi en pool med 64 trådar med fast tråd, som vi skapar genom att kalla Executors.newFixedThreadPool(int)fabriksmetoden. Listning 1 visar implementeringen av huvudklassen.

Listning 1. Konstruera en 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(); } }

I ovanstående WebCrawler6konstruktör skapar vi en trådbunden fast storlek med 64 trådar. Vi startar sedan programmet med att anropa startCrawlingmetoden, som skapar den första tråden och skickar den till ExecutorService.

Därefter skapar vi ett LinkHandlergränssnitt som exponerar hjälpmetoder för att interagera med webbadresser. Kraven är följande: (1) markera en URL som besökt med addVisited()metoden; (2) få antalet besökta webbadresser genom size()metoden; (3) avgöra om en URL redan har besökts med visited()metoden; och (4) lägga till en ny URL i kön genom queueLink()metoden.

Listing 2. LinkHandler-gränssnittet

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

Nu när vi genomsöker sidor måste vi starta resten av trådarna, vilket vi gör via LinkFindergränssnittet, som visas i Listing 3. Notera linkHandler.queueLink(l)raden.

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

Logiken LinkFinderär enkel: (1) vi börjar analysera en URL; (2) efter att vi har samlat alla länkar på motsvarande sida markerar vi sidan som besökt; och (3) vi skickar varje hittad länk till en kö genom att anropa queueLink()metoden. Denna metod skapar faktiskt en ny tråd och skickar den till ExecutorService. Om "fria" trådar finns tillgängliga i poolen kommer tråden att köras; annars placeras den i en väntekö. När vi har nått 1500 olika besökta länkar skriver vi ut statistiken och programmet fortsätter att köras.

En Java 7-sökrobot med ForkJoinPool

Fork / Join-ramverket som introducerades i Java 7 är faktiskt en implementering av Divide and Conquer-algoritmen (se Resurser), där en central ForkJoinPoolutför förgreningar ForkJoinTask. I det här exemplet använder vi en ForkJoinPool"backad" av 64 trådar. Jag säger backad eftersom ForkJoinTasks är lättare än trådar. I Fork / Join kan ett stort antal uppgifter vara värd för ett mindre antal trådar.

På samma sätt som Java 6-implementeringen, börjar vi med att snabba i WebCrawler7konstruktören ett ForkJoinPoolobjekt som stöds av 64 trådar.

Lista 4. Java 7 LinkHandler-implementering

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

Observera att LinkHandlergränssnittet i Listing 4 är nästan detsamma som Java 6-implementeringen från Listing 2. Det saknas bara queueLink()metoden. De viktigaste metoderna att titta på är konstruktören och startCrawling()metoden. I konstruktören skapar vi en ny ForkJoinPoolmed 64 trådar. (Jag har valt 64 trådar istället för 50 eller något annat ForkJoinPoolrundnummer eftersom det i Javadoc säger att antalet trådar måste vara en kraft på två.) Poolen åberopar en ny LinkFinderAction, som kommer att rekursivt åberopa ytterligare ForkJoinTasks. Listning 5 visar LinkFinderActionklassen: