자바 팁 : ForkJoinPool과 ExecutorService를 사용하는 경우

Java 7에 도입 된 Fork / Join 라이브러리는 멀티 코어 시스템의 핵심 기능인 하드웨어 병렬 처리를 지원하여 기존 Java 동시성 패키지를 확장합니다. 이 Java 팁에서 Madalin Ilie 는 웹 크롤러 애플리케이션에서 Java 6 ExecutorService클래스를 Java 7 로 대체 할 때의 성능 영향을 보여줍니다 ForkJoinPool.

웹 스파이더라고도하는 웹 크롤러는 검색 엔진 성공의 핵심입니다. 이러한 프로그램은 웹을 지속적으로 스캔하여 수백만 페이지의 데이터를 수집하여 검색 엔진 데이터베이스로 다시 보냅니다. 그런 다음 데이터가 인덱싱되고 알고리즘 적으로 처리되어 더 빠르고 정확한 검색 결과를 얻을 수 있습니다. 검색 최적화에 가장 유명하지만 웹 크롤러는 링크 유효성 검사 또는 웹 페이지 모음에서 특정 데이터 (예 : 이메일 주소)를 찾고 반환하는 것과 같은 자동화 된 작업에도 사용할 수 있습니다.

구조적으로 대부분의 웹 크롤러는 비교적 단순한 기능과 요구 사항이 있지만 고성능 멀티 스레드 프로그램입니다. 따라서 웹 크롤러를 구축하는 것은 멀티 스레드 또는 동시 프로그래밍 기술을 비교하고 실행하는 흥미로운 방법입니다.

Java Tips의 귀환!

Java 팁은 JavaWorld 독자가 프로그래밍 기술과 발견을 공유하도록 초대하는 짧은 코드 중심 기사입니다. JavaWorld 커뮤니티와 공유 할 팁이 있으면 알려주십시오. 또한 동료로부터 더 많은 프로그래밍 팁을 보려면 Java Tips Archive를 확인하십시오.

이 기사에서는 웹 크롤러를 작성하는 두 가지 접근 방식을 살펴 보겠습니다. 하나는 Java 6 ExecutorService를 사용하고 다른 하나는 Java 7의 ForkJoinPool을 사용합니다. 예제를 따르려면 개발 환경에 Java 7 업데이트 2와 타사 라이브러리 HtmlParser가 설치되어 있어야합니다.

Java 동시성에 대한 두 가지 접근 방식

ExecutorService클래스는 java.util.concurrentJava 5 (물론 Java 6의 일부)에 도입 된 혁명의 일부로, Java 플랫폼에서 스레드 처리를 단순화했습니다. ExecutorService비동기 작업의 진행 추적 및 종료를 관리하는 메서드를 제공하는 Executor입니다. 를 도입하기 전에 java.util.concurrentJava 개발자는 타사 라이브러리에 의존하거나 자체 클래스를 작성하여 프로그램의 동시성을 관리했습니다.

Java 7에 도입 된 Fork / Join은 기존 동시성 유틸리티 클래스를 대체하거나 경쟁하기위한 것이 아닙니다. 대신 업데이트하고 완료합니다. Fork / Join 은 Java 프로그램에서 분할 및 정복 또는 재귀 작업 처리 의 필요성을 해결합니다 (참고 자료 참조).

Fork / Join의 논리는 매우 간단합니다. (1) 각각의 큰 작업을 작은 작업으로 분리 (fork)합니다. (2) 각 작업을 별도의 스레드에서 처리합니다 (필요한 경우 더 작은 작업으로 분리). (3) 결과에 참여하십시오.

다음 두 가지 웹 크롤러 구현은 Java 6 ExecutorService및 Java 7 의 기능을 보여주는 간단한 프로그램입니다 ForkJoinPool.

웹 크롤러 빌드 및 벤치마킹

웹 크롤러의 임무는 링크를 찾아 따라가는 것입니다. 목적은 링크 유효성 검사이거나 데이터 수집 일 수 있습니다. (예를 들어 웹에서 Angelina Jolie 또는 Brad Pitt의 사진을 검색하도록 프로그램에 지시 할 수 있습니다.)

애플리케이션 아키텍처는 다음으로 구성됩니다.

  1. 링크와 상호 작용하는 기본 작업을 노출하는 인터페이스입니다. 즉, 방문한 링크의 수를 가져오고, 방문 할 새 링크를 대기열에 추가하고, 링크를 방문한 것으로 표시합니다.
  2. 응용 프로그램의 시작점이 될이 인터페이스의 구현
  3. 링크가 이미 방문되었는지 확인하기 위해 비즈니스 로직을 보유하는 스레드 / 재귀 적 작업입니다. 그렇지 않은 경우 해당 페이지의 모든 링크를 수집하고 새 스레드 / 재귀 작업을 만든 다음 ExecutorService또는ForkJoinPool
  4. ExecutorService또는 ForkJoinPool대기 작업을 처리하기 위해

해당 페이지의 모든 링크가 반환 된 후 링크는 "방문한"것으로 간주됩니다.

Java 6 및 Java 7에서 사용 가능한 동시성 도구를 사용하여 개발 용이성을 비교하는 것 외에도 두 가지 벤치 마크를 기반으로 애플리케이션 성능을 비교합니다.

  • 검색 범위 : 1,500 개의 서로 다른 링크 를 방문하는 데 필요한 시간 측정
  • 처리 능력 : 3,000 개의 서로 다른 링크 를 방문하는 데 필요한 시간을 초 단위로 측정합니다 . 이것은 인터넷 연결이 처리하는 초당 킬로 비트 수를 측정하는 것과 같습니다.

비교적 간단하지만 이러한 벤치 마크는 특정 애플리케이션 요구 사항에 대해 Java 6과 Java 7의 Java 동시성 성능에 대한 최소한의 창을 제공합니다.

ExecutorService로 빌드 된 Java 6 웹 크롤러

Java 6 웹 크롤러 구현의 경우 Executors.newFixedThreadPool(int)팩토리 메소드 를 호출하여 생성하는 64 개 스레드의 고정 스레드 풀을 사용합니다 . 목록 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.

다음 LinkHandler으로 URL과 상호 작용하는 도우미 메서드를 노출 하는 인터페이스를 만듭니다 . 요구 사항은 다음과 같습니다. (1) addVisited()방법을 사용하여 URL을 방문한 것으로 표시합니다 . (2) size()메소드를 통해 방문한 URL의 수를 가져옵니다 . (3) visited()방법을 사용하여 URL이 이미 방문되었는지 확인합니다 . 그리고 (4) queueLink()메서드를 통해 대기열에 새 URL을 추가합니다 .

목록 2. 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); }

이제 페이지를 크롤링 할 때 나머지 스레드를 시작해야합니다.이 작업 LinkFinder은 목록 3에 표시된 것처럼 인터페이스 를 통해 수행됩니다 linkHandler.queueLink(l).

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

의 논리 LinkFinder는 간단합니다. (1) URL 구문 분석을 시작합니다. (2) 해당 페이지 내의 모든 링크를 수집 한 후 해당 페이지를 방문한 것으로 표시합니다. (3) queueLink()메서드 를 호출하여 발견 된 각 링크를 큐에 보냅니다 . 이 메서드는 실제로 새 스레드를 만들고 ExecutorService. 풀에서 "사용 가능한"스레드를 사용할 수있는 경우 스레드가 실행됩니다. 그렇지 않으면 대기 대기열에 배치됩니다. 방문한 1,500 개의 개별 링크에 도달 한 후 통계를 인쇄하고 프로그램이 계속 실행됩니다.

ForkJoinPool을 사용하는 Java 7 웹 크롤러

Java 7에 도입 된 Fork / Join 프레임 워크는 실제로 중앙에서 ForkJoinPool분기를 실행 하는 Divide and Conquer 알고리즘 (참고 자료 참조)의 구현입니다 ForkJoinTask. 이 예 ForkJoinPool에서는 64 개의 스레드에 의해 "backed"를 사용 합니다. 나는 s가 스레드보다 가볍기 때문에 backed 라고 말합니다 ForkJoinTask. Fork / Join에서는 적은 수의 스레드에서 많은 수의 작업을 호스팅 할 수 있습니다.

Java 6 구현과 유사하게 WebCrawler7생성자에서 ForkJoinPool64 개의 스레드가 지원 하는 객체 를 인스턴스화하는 것으로 시작 합니다.

목록 4. 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(); } }

LinkHandler목록 4 의 인터페이스는 목록 2의 Java 6 구현과 거의 동일합니다 queueLink(). 메소드가 누락 된 것뿐입니다 . 살펴 봐야 할 가장 중요한 메서드는 생성자와 startCrawling()메서드입니다. 생성자에서 ForkJoinPool64 개의 스레드가 지원 하는 새 파일을 만듭니다 . ( ForkJoinPoolJavadoc에서는 스레드 수가 2의 거듭 제곱이어야한다고 명시하고 있기 때문에 50 개 또는 다른 라운드 수 대신 64 개의 스레드를 선택 했습니다.) 풀은 new를 LinkFinderAction호출하여 추가를 재귀 적으로 호출 ForkJoinTasks합니다. 목록 5는 LinkFinderAction클래스를 보여줍니다 .