Съвет за Java: Кога да се използва ForkJoinPool срещу ExecutorService

Библиотеката Fork / Join, въведена в Java 7, разширява съществуващия пакет за съвпадение на Java с поддръжка на хардуерен паралелизъм, ключова характеристика на многоядрените системи. В този съвет за Java Madalin Ilie демонстрира въздействието върху производителността на замяната на ExecutorServiceклас Java 6 с Java 7 ForkJoinPoolв приложение за уеб обхождане.

Уеб роботите, известни още като уеб паяци, са ключови за успеха на търсачките. Тези програми непрекъснато сканират мрежата, събират милиони страници данни и ги изпращат обратно към базите данни на търсачките. След това данните се индексират и обработват алгоритмично, което води до по-бързи и по-точни резултати от търсенето. Въпреки че те са най-известни за оптимизиране на търсенето, уеб роботите могат да се използват и за автоматизирани задачи като валидиране на връзки или намиране и връщане на конкретни данни (като имейл адреси) в колекция от уеб страници.

В архитектурно отношение повечето уеб браузъри са високопроизводителни многонишкови програми, макар и с относително проста функционалност и изисквания. Следователно изграждането на уеб робот е интересен начин за практикуване, както и за сравняване, многонишкови или едновременни техники за програмиране.

Завръщането на Java Tips!

Java Tips са кратки статии, задвижвани с код, които канят читателите на JavaWorld да споделят своите умения за програмиране и открития. Уведомете ни, ако имате съвет, който да споделите с общността на JavaWorld. Разгледайте и архива на Java Tips за още съвети за програмиране от вашите връстници.

В тази статия ще разгледам два подхода за писане на уеб робот: единият използва Java 6 ExecutorService, а другият Java 7 ForkJoinPool. За да следвате примерите, ще трябва да имате (към настоящия момент) Java 7 актуализация 2, инсталирана във вашата среда за разработка, както и библиотеката на трети страни HtmlParser.

Два подхода към едновременността на Java

Най- ExecutorServiceклас е част от java.util.concurrentреволюцията, въведена в Java 5 (и част от Java 6, разбира се), който опростява конец за обработка на платформата Java. ExecutorServiceе изпълнител, който предоставя методи за управление на проследяването на напредъка и прекратяването на асинхронни задачи. Преди въвеждането на java.util.concurrent, разработчиците на Java разчитаха на библиотеки на трети страни или написаха свои собствени класове, за да управляват едновременността в своите програми.

Fork / Join, въведена в Java 7, не е предназначена да замени или да се конкурира със съществуващите класове на помощните паралели; вместо това ги актуализира и допълва. Fork / Join адресира необходимостта от разделяне и завладяване или рекурсивна обработка на задачи в Java програми (вж. Ресурси).

Логиката на Fork / Join е много проста: (1) отделете (разклонете) всяка голяма задача на по-малки задачи; (2) обработвайте всяка задача в отделна нишка (разделяйки тези на още по-малки задачи, ако е необходимо); (3) присъединете се към резултатите.

Следващите две изпълнения на уеб обхождане са прости програми, които демонстрират характеристиките и функционалността на Java 6 ExecutorServiceи Java 7 ForkJoinPool.

Изграждане и сравнителен анализ на уеб робота

Задачата на нашия уеб робот ще бъде да намира и следва връзки. Целта му може да бъде валидиране на връзки или събиране на данни. (Можете например да инструктирате програмата да търси в мрежата снимки на Анджелина Джоли или Брад Пит.)

Архитектурата на приложението се състои от следното:

  1. Интерфейс, който излага основните операции за взаимодействие с връзки; т.е. вземете броя на посетените връзки, добавете нови връзки, които да бъдат посетени на опашка, маркирайте връзка като посетена
  2. Приложение за този интерфейс, което ще бъде и отправна точка на приложението
  3. Нишка / рекурсивно действие, което ще задържи бизнес логиката, за да провери дали дадена връзка вече е била посетена. Ако не, той ще събере всички връзки в съответната страница, ще създаде нова нишка / рекурсивна задача и ще я изпрати на ExecutorServiceилиForkJoinPool
  4. Един ExecutorServiceили ForkJoinPoolда се справят с чакащи задачи

Имайте предвид, че връзка се счита за „посетена“, след като всички връзки в съответната страница са върнати.

В допълнение към сравнението на лекотата на разработка, използвайки инструментите за паралелност, налични в Java 6 и Java 7, ще сравним производителността на приложението въз основа на два еталона:

  • Обхват на търсенето : Измерва времето, необходимо за посещение на 1500 различни връзки
  • Мощност за обработка : Измерва времето в секунди, необходимо за посещение на 3000 неразлични връзки; това е като да измервате колко килобита в секунда обработва вашата интернет връзка.

Макар и сравнително прости, тези бенчмаркове ще осигурят поне малък прозорец към производителността на едновременността на Java в Java 6 срещу Java 7 за определени изисквания на приложението.

Уеб робот Java 6, създаден с ExecutorService

За изпълнението на уеб робота на Java 6 ще използваме пул с фиксирана нишка от 64 нишки, който създаваме чрез извикване на Executors.newFixedThreadPool(int)фабричния метод. Листинг 1 показва изпълнението на основния клас.

Листинг 1. Изграждане на 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(); } }

В горния WebCrawler6конструктор създаваме пул от нишки с фиксиран размер от 64 нишки. След това стартираме програмата, като извикаме startCrawlingметода, който създава първата нишка и я изпраща на ExecutorService.

Next, we create a LinkHandler interface, which exposes helper methods to interact with URLs. Requirements are as follows: (1) mark a URL as visited using the addVisited() method; (2) get the number of the visited URLs through the size() method; (3) determine whether a URL has been already visited using the visited() method; and (4) add a new URL in the queue through the queueLink() method.

Listing 2. The LinkHandler interface

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

Now, as we crawl pages, we need to start up the rest of the threads, which we do via the LinkFinder interface, as shown in Listing 3. Note the linkHandler.queueLink(l) line.

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

The logic of the LinkFinder is simple: (1) we start parsing a URL; (2) after we gather all the links within the corresponding page, we mark the page as visited; and (3) we send each found link to a queue by calling the queueLink() method. This method will actually create a new thread and send it to the ExecutorService. If "free" threads are available in the pool, the thread will be executed; otherwise it will be placed in a waiting queue. After we reach 1,500 distinct links visited, we print the statistics and the program continues to run.

A Java 7 web crawler with 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(); } }

Имайте предвид, че LinkHandlerинтерфейсът в Листинг 4 е почти същият като внедряването на Java 6 от Листинг 2. Липсва само queueLink()методът. Най-важните методи за разглеждане са конструкторът и startCrawling()методът. В конструктора създаваме нов, ForkJoinPoolподкрепен от 64 нишки. (Избрах 64 нишки вместо 50 или някакво друго кръгло число, тъй като в ForkJoinPoolJavadoc се посочва, че броят на нишките трябва да бъде степен две.) Пулът извиква нов LinkFinderAction, който ще се извиква рекурсивно допълнително ForkJoinTasks. Листинг 5 показва LinkFinderActionкласа: