Wskazówka dotycząca języka Java: kiedy używać ForkJoinPool, a kiedy ExecutorService

Biblioteka Fork / Join wprowadzona w Javie 7 rozszerza istniejący pakiet współbieżności Java o obsługę równoległości sprzętowej, kluczowej funkcji systemów wielordzeniowych. W tej wskazówce dotyczącej języka Java Madalin Ilie demonstruje wpływ na wydajność zastąpienia ExecutorServiceklasy Java 6 przez Java 7 ForkJoinPoolw aplikacji przeszukiwacza sieci WWW.

Przeszukiwacze sieci, znane również jako pająki internetowe, są kluczem do sukcesu wyszukiwarek. Programy te nieustannie skanują sieć, zbierając miliony stron danych i wysyłając je z powrotem do baz danych wyszukiwarek. Dane są następnie indeksowane i przetwarzane algorytmicznie, co skutkuje szybszymi i dokładniejszymi wynikami wyszukiwania. Chociaż są one najbardziej znane do optymalizacji wyszukiwania, roboty internetowe mogą być również używane do automatycznych zadań, takich jak sprawdzanie poprawności linków lub znajdowanie i zwracanie określonych danych (takich jak adresy e-mail) w zbiorze stron internetowych.

Pod względem architektonicznym większość robotów indeksujących to wysokowydajne programy wielowątkowe, aczkolwiek o stosunkowo prostej funkcjonalności i wymaganiach. Tworzenie robota indeksującego jest zatem interesującym sposobem ćwiczenia, a także porównywania, wielowątkowych lub współbieżnych technik programowania.

Powrót porad dotyczących języka Java!

Wskazówki dotyczące języka Java to krótkie artykuły oparte na kodzie, które zachęcają czytelników JavaWorld do dzielenia się swoimi umiejętnościami programowania i odkryciami. Daj nam znać, jeśli chcesz podzielić się wskazówką ze społecznością JavaWorld. Zapoznaj się również z archiwum porad dotyczących języka Java, aby uzyskać więcej wskazówek dotyczących programowania od innych użytkowników.

W tym artykule omówię dwa podejścia do pisania robota indeksującego: jedno przy użyciu Java 6 ExecutorService, a drugie przy użyciu ForkJoinPool Javy 7. Aby postępować zgodnie z przykładami, musisz mieć (w chwili pisania tego tekstu) Java 7 Update 2 zainstalowaną w swoim środowisku programistycznym, a także bibliotekę HtmlParser innej firmy.

Dwa podejścia do współbieżności języka Java

ExecutorServiceKlasa część java.util.concurrentobrotu 5 wprowadzony w Java (Java i części 6, w trakcie), który uproszczonej gwint obsługi na platformie Java. ExecutorServiceto moduł wykonawczy, który zapewnia metody zarządzania śledzeniem postępu i kończeniem zadań asynchronicznych. Przed wprowadzeniem programu java.util.concurrentprogramiści Java polegali na bibliotekach innych firm lub pisali własne klasy, aby zarządzać współbieżnością w swoich programach.

Fork / Join, wprowadzony w Javie 7, nie ma na celu zastąpienia ani konkurowania z istniejącymi klasami narzędziowymi współbieżności; zamiast tego aktualizuje je i uzupełnia. Fork / Join odpowiada na potrzebę dziel i rządź, czyli rekurencyjne przetwarzanie zadań w programach Java (zobacz Zasoby).

Logika Fork / Join jest bardzo prosta: (1) rozdziel (podziel) każde duże zadanie na mniejsze; (2) przetwarzaj każde zadanie w osobnym wątku (dzieląc je na jeszcze mniejsze zadania, jeśli to konieczne); (3) dołącz wyniki.

Dwie poniższe implementacje przeszukiwacza sieci WWW to proste programy, które demonstrują cechy i funkcjonalność Java 6 ExecutorServicei Java 7 ForkJoinPool.

Tworzenie i testowanie robota indeksującego

Zadaniem naszego robota internetowego będzie znajdowanie i podążanie za linkami. Jego celem może być sprawdzanie poprawności linków lub gromadzenie danych. (Możesz na przykład poinstruować program, aby przeszukał Internet w poszukiwaniu zdjęć Angeliny Jolie lub Brada Pitta).

Architektura aplikacji składa się z następujących elementów:

  1. Interfejs, który udostępnia podstawowe operacje do interakcji z linkami; tzn. uzyskać liczbę odwiedzonych linków, dodać nowe linki do odwiedzenia w kolejce, oznaczyć link jako odwiedzony
  2. Implementacja tego interfejsu, która będzie również punktem wyjścia aplikacji
  3. Wątek / akcja rekurencyjna, która będzie przechowywać logikę biznesową w celu sprawdzenia, czy łącze zostało już odwiedzone. Jeśli nie, zbierze wszystkie linki na odpowiedniej stronie, utworzy nowy wątek / zadanie rekurencyjne i wyśle ​​go do ExecutorServicelubForkJoinPool
  4. ExecutorServiceLub ForkJoinPooldo obsługi zadań oczekujących

Zwróć uwagę, że łącze jest uważane za „odwiedzone” po zwróceniu wszystkich linków na odpowiedniej stronie.

Oprócz porównania łatwości programowania przy użyciu narzędzi współbieżności dostępnych w językach Java 6 i Java 7, porównamy wydajność aplikacji na podstawie dwóch testów:

  • Zakres wyszukiwania : mierzy czas wymagany do odwiedzenia 1500 różnych linków
  • Moc obliczeniowa : mierzy czas w sekundach potrzebny do odwiedzenia 3000 nierozróżnialnych linków; to tak, jakby mierzyć, ile kilobitów na sekundę przetwarza połączenie internetowe.

Chociaż te testy porównawcze są stosunkowo proste, zapewnią przynajmniej małe okno na wydajność współbieżności Java w Javie 6 w porównaniu z Javą 7 w przypadku niektórych wymagań aplikacji.

Przeszukiwacz sieciowy Java 6 zbudowany przy użyciu usługi ExecutorService

Do implementacji przeszukiwacza WWW Java 6 użyjemy stałej puli 64 wątków, którą tworzymy, wywołując Executors.newFixedThreadPool(int)metodę fabryczną. Listing 1 przedstawia główną implementację klasy.

Listing 1. Konstruowanie robota internetowego

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

W powyższym WebCrawler6konstruktorze tworzymy pulę wątków o stałym rozmiarze składającą się z 64 wątków. Następnie uruchamiamy program, wywołując startCrawlingmetodę, która tworzy pierwszy wątek i przesyła go do pliku ExecutorService.

Następnie tworzymy LinkHandlerinterfejs, który udostępnia metody pomocnicze do interakcji z adresami URL. Wymagania są następujące: (1) oznacz adres URL jako odwiedzany przy użyciu addVisited()metody; (2) uzyskać liczbę odwiedzonych adresów URL za pomocą size()metody; (3) określić, czy dany adres URL był już odwiedzany za pomocą tej visited()metody; i (4) dodaj nowy adres URL do kolejki za pomocą queueLink()metody.

Listing 2. Interfejs 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); }

Teraz, gdy przeszukujemy strony, musimy uruchomić pozostałe wątki, co robimy przez LinkFinderinterfejs, jak pokazano na Listingu 3. Zwróć uwagę na linkHandler.queueLink(l)linię.

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

Logika tego LinkFinderjest prosta: (1) zaczynamy analizować adres URL; (2) po zebraniu wszystkich linków na odpowiedniej stronie oznaczamy ją jako odwiedzoną; i (3) wysyłamy każdy znaleziony link do kolejki, wywołując queueLink()metodę. Ta metoda faktycznie utworzy nowy wątek i wyśle ​​go do ExecutorService. Jeśli w puli są dostępne „wolne” wątki, zostanie on wykonany; w przeciwnym razie zostanie umieszczony w kolejce oczekiwania. Po osiągnięciu 1500 różnych odwiedzonych linków, drukujemy statystyki i program dalej działa.

Robot sieciowy Java 7 z ForkJoinPool

Struktura Fork / Join wprowadzona w Javie 7 jest w rzeczywistości implementacją algorytmu Divide and Conquer (patrz Zasoby), w którym centrala ForkJoinPoolwykonuje rozgałęzienia ForkJoinTask. W tym przykładzie użyjemy ForkJoinPool„wspieranego” przez 64 wątki. Mówię z zabezpieczeniem, ponieważ ForkJoinTasksą lżejsze niż nici. W Fork / Join duża liczba zadań może być obsługiwana przez mniejszą liczbę wątków.

Podobnie jak w przypadku implementacji Java 6, zaczynamy od utworzenia w WebCrawler7konstruktorze ForkJoinPoolobiektu wspieranego przez 64 wątki.

Listing 4. Implementacja 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(); } }

Zauważ, że LinkHandlerinterfejs na liście 4 jest prawie taki sam jak w implementacji Java 6 z listy 2. Brakuje tylko queueLink()metody. Najważniejsze metody, na które należy zwrócić uwagę, to konstruktor i startCrawling()metoda. W konstruktorze tworzymy nowy ForkJoinPoolwspierany przez 64 wątki. (Wybrałem 64 wątki zamiast 50 lub inną okrągłą liczbę, ponieważ w ForkJoinPoolJavadoc stwierdza się, że liczba wątków musi być potęgą dwóch). Pula wywołuje nową LinkFinderAction, która będzie rekurencyjnie wywoływać dalej ForkJoinTasks. Listing 5 przedstawia LinkFinderActionklasę: