Java savjet: Kada koristiti ForkJoinPool vs ExecutorService

Biblioteka Fork / Join predstavljena u Javi 7 proširuje postojeći paket Java konkurentnosti s podrškom za hardverski paralelizam, ključnu značajku višejezgrenih sustava. U ovom Java Savjetu Madalin Ilie pokazuje učinak zamjene ExecutorServiceklase Java 6 s Java 7 ForkJoinPoolu aplikaciji za indeksiranje weba.

Alati za indeksiranje weba, poznati i kao web pauci, ključni su za uspjeh pretraživača. Ovi programi neprestano skeniraju web, prikupljaju milijune stranica podataka i šalju ih natrag u baze podataka pretraživača. Podaci se zatim indeksiraju i obrađuju algoritamski, što rezultira bržim i preciznijim rezultatima pretraživanja. Iako su najpoznatiji za optimizaciju pretraživanja, alati za indeksiranje također se mogu koristiti za automatizirane zadatke poput provjere valjanosti veza ili pronalaženja i vraćanja određenih podataka (kao što su adrese e-pošte) u zbirci web stranica.

Arhitektonski, većina web alata za indeksiranje predstavljaju višenamenske programe visokih performansi, iako s relativno jednostavnom funkcionalnošću i zahtjevima. Izgradnja alata za indeksiranje weba stoga je zanimljiv način vježbanja, kao i uspoređivanje, višenitnih ili istodobnih tehnika programiranja.

Povratak Java savjeta!

Java Savjeti su kratki članci vođeni kodovima koji pozivaju čitatelje JavaWorlda da podijele svoje programske vještine i otkrića. Javite nam ako imate savjet koji želite podijeliti sa zajednicom JavaWorld. Također pogledajte Java arhivu savjeta za više savjeta o programiranju svojih vršnjaka.

U ovom ću članku proći kroz dva pristupa pisanju web alata za indeksiranje: jedan koji koristi Java 6 ExecutorService, a drugi Java 7 ForkJoinPool. Da biste slijedili primjere, morat ćete instalirati (od pisanja ovog teksta) Javu 7 ažuriranja 2 u vašem razvojnom okruženju, kao i biblioteku treće strane HtmlParser.

Dva pristupa paralelnosti Java

Predavanje ExecutorServiceje dio java.util.concurrentrevolucije uvedene u Javi 5 (i dio Jave 6, naravno), koja je pojednostavila rukovanje nitima na Java platformi. ExecutorServiceje izvršitelj koji pruža metode za upravljanje praćenjem napretka i prekidom asinkronih zadataka. Prije uvođenja java.util.concurrent, programeri Jave oslanjali su se na biblioteke trećih strana ili su napisali vlastite razrede kako bi upravljali istodobnošću u svojim programima.

Fork / Join, uveden u Javi 7, nije namijenjen zamjeni ili nadmetanju s postojećim klasa pomoćnih parametara; umjesto toga ih ažurira i dovršava. Fork / Join rješava potrebu za podijeli i osvoji ili rekurzivnu obradu zadataka u Java programima (vidi Resursi).

Logika Fork / Join je vrlo jednostavna: (1) odvojite (račvajte) svaki veliki zadatak na manje zadatke; (2) obraditi svaki zadatak u zasebnoj niti (razdvajajući ih u još manje zadatke ako je potrebno); (3) pridružite se rezultatima.

Sljedeće dvije implementacije web-indeksiranja jednostavni su programi koji pokazuju značajke i funkcionalnost Java 6 ExecutorServicei Java 7 ForkJoinPool.

Izgradnja i usporedba web alata za indeksiranje

Zadatak našeg alata za indeksiranje bit će pronaći i slijediti veze. Njegova svrha može biti provjera valjanosti veze ili prikupljanje podataka. (Možete, na primjer, naložiti programu da na webu traži slike Angeline Jolie ili Brada Pitta.)

Arhitektura aplikacije sastoji se od sljedećeg:

  1. Sučelje koje izlaže osnovne operacije za interakciju s vezama; tj. dobiti broj posjećenih veza, dodati nove veze koje će se posjetiti u redu čekanja, označiti vezu kao posjećenu
  2. Implementacija za ovo sučelje koje će ujedno biti i početna točka aplikacije
  3. Nit / rekurzivna radnja koja će zadržati poslovnu logiku radi provjere je li veza već posjećena. Ako nije, prikupit će sve veze na odgovarajućoj stranici, stvoriti novu nit / rekurzivni zadatak i predati ga na ExecutorServiceiliForkJoinPool
  4. ExecutorServiceIli ForkJoinPoolda obrađuju zadatke na čekanju

Imajte na umu da se veza smatra "posjećenom" nakon što su vraćene sve veze na odgovarajućoj stranici.

Osim usporedbe jednostavnosti razvoja pomoću alata za istodobnost dostupnih u Java 6 i Java 7, usporedit ćemo izvedbu aplikacija na temelju dva mjerila:

  • Pokrivenost pretraživanja : Mjeri vrijeme potrebno za posjet 1500 različitih veza
  • Procesorska snaga : mjeri vrijeme u sekundama potrebno za posjet 3000 nerazlikovanih veza; ovo je poput mjerenja koliko kilobita u sekundi obrađuje vaša internetska veza.

Iako su relativno jednostavna, ova će mjerila pružiti barem mali prozor u izvedbu Java istovremenosti u Javi 6 nasuprot Javi 7 za određene zahtjeve aplikacije.

Alat za indeksiranje Java 6 izrađen pomoću ExecutorService

Za implementaciju web pretraživača Java 6 koristit ćemo spremište fiksnih niti od 64 niti koje kreiramo pozivanjem Executors.newFixedThreadPool(int)tvorničke metode. Popis 1 prikazuje implementaciju glavne klase.

Popis 1. Izrada WebCrawlera

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

U gore navedenom WebCrawler6konstruktoru kreiramo spremište niti fiksne veličine od 64 niti. Zatim započinjemo program pozivanjem startCrawlingmetode koja kreira prvu nit i preda je datoteci ExecutorService.

Dalje, kreiramo LinkHandlersučelje koje izlaže pomoćne metode za interakciju s URL-ovima. Zahtjevi su sljedeći: (1) označite URL kao posjećen uporabom addVisited()metode; (2) dobiti size()metodu broja posjećenih URL-ova ; (3) utvrditi je li URL već posjećen pomoću visited()metode; i (4) dodavanje novog URL-a u red čekanja putem queueLink()metode.

Popis 2. Sučelje 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); }

Sada, dok indeksiramo stranice, moramo pokrenuti ostatak niti, što radimo putem LinkFindersučelja, kao što je prikazano na popisu 3. Zabilježite linkHandler.queueLink(l)redak.

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

Logika LinkFinderje jednostavna: (1) započinjemo raščlanjivanje URL-a; (2) nakon što prikupimo sve poveznice unutar odgovarajuće stranice, stranicu označavamo kao posjećenu; i (3) svaku pronađenu vezu šaljemo u red pozivanjem queueLink()metode. Ova će metoda zapravo stvoriti novu nit i poslati je na ExecutorService. Ako su u spremištu dostupne "besplatne" niti, nit će se izvršiti; u protivnom bit će stavljen u red čekanja. Nakon što dosegnemo 1500 različitih posjećenih veza, ispisujemo statistiku i program se nastavlja izvoditi.

Alat za indeksiranje Java 7 s ForkJoinPool

The Fork/Join framework introduced in Java 7 is actually an implementation of the Divide and Conquer algorithm (see Resources), in which a central ForkJoinPool executes branching ForkJoinTasks. For this example we'll use a ForkJoinPool "backed" by 64 threads. I say backed because ForkJoinTasks are lighter than threads. In Fork/Join, a large number of tasks can be hosted by a smaller number of threads.

Similar to the Java 6 implementation, we start by instantiating in the WebCrawler7 constructor a ForkJoinPool object backed by 64 threads.

Listing 4. Java 7 LinkHandler implementation

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

Imajte na umu da je LinkHandlersučelje na popisu 4 gotovo isto kao implementacija Java 6 s popisa 2. Nedostaje samo queueLink()metoda. Najvažnije metode koje treba pogledati su konstruktor i startCrawling()metoda. U konstruktoru izrađujemo novu ForkJoinPoolpotpomognutu sa 64 niti. (Odabrao sam 64 niti umjesto 50 ili neki drugi okrugli broj, jer u ForkJoinPoolJavadocu stoji da broj niti mora biti stepen dvoje.) Skupina poziva novu LinkFinderAction, koja će se rekurzivno pozivati ​​dalje ForkJoinTasks. Popis 5 prikazuje LinkFinderActionklasu: