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- ExecutorService
klassen med Java 7 ForkJoinPool
i 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 ExecutorService
klass är en del av java.util.concurrent
revolutionen 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 ExecutorService
och 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:
- 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
- En implementering för detta gränssnitt som också kommer att vara startpunkten för applikationen
- 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
ExecutorService
ellerForkJoinPool
- En
ExecutorService
eller förForkJoinPool
att 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 WebCrawler6
konstruktör skapar vi en trådbunden fast storlek med 64 trådar. Vi startar sedan programmet med att anropa startCrawling
metoden, som skapar den första tråden och skickar den till ExecutorService
.
Därefter skapar vi ett LinkHandler
grä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 LinkFinder
grä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 ForkJoinPool
utför förgreningar ForkJoinTask
. I det här exemplet använder vi en ForkJoinPool
"backad" av 64 trådar. Jag säger backad eftersom ForkJoinTask
s ä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 WebCrawler7
konstruktören ett ForkJoinPool
objekt 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 LinkHandler
grä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 ForkJoinPool
med 64 trådar. (Jag har valt 64 trådar istället för 50 eller något annat ForkJoinPool
rundnummer 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 LinkFinderAction
klassen: