Javaのヒント:ForkJoinPoolとExecutorServiceをいつ使用するか

Java7で導入されたFork / Joinライブラリは、マルチコアシステムの重要な機能であるハードウェア並列処理をサポートすることで、既存のJava同時実行パッケージを拡張します。このJavaのヒントMadalinイリーではJava 6の交換のパフォーマンスへの影響を示していExecutorServiceたJava 7人の持つクラスをForkJoinPoolウェブクローラアプリケーションでは。

Webスパイダーとしても知られるWebクローラーは、検索エンジンの成功の鍵です。これらのプログラムは永続的にWebをスキャンし、数百万ページのデータを収集して検索エンジンデータベースに送り返します。次に、データにインデックスが付けられ、アルゴリズムで処理されるため、より高速で正確な検索結果が得られます。これらは検索の最適化に最もよく使用されていますが、Webクローラーは、リンクの検証や、Webページのコレクション内の特定のデータ(電子メールアドレスなど)の検索と返送などの自動化されたタスクにも使用できます。

アーキテクチャ的には、ほとんどのWebクローラーは、比較的単純な機能と要件を備えていますが、高性能のマルチスレッドプログラムです。したがって、Webクローラーを構築することは、プログラミング手法を比較、マルチスレッド、または並行して実行するための興味深い方法です。

Javaのヒントが戻ってきました!

Javaのヒントは、JavaWorldの読者にプログラミングのスキルと発見を共有するように勧める、短いコード駆動型の記事です。JavaWorldコミュニティと共有するためのヒントがあればお知らせください。また、ピアからのプログラミングのヒントについては、Javaヒントアーカイブを確認してください。

この記事では、Webクローラーを作成するための2つのアプローチについて説明します。1つはJava 6 ExecutorServiceを使用し、もう1つはJava7のForkJoinPoolを使用します。例に従うには、(この記事の執筆時点で)開発環境にJava 7 update 2と、サードパーティライブラリHtmlParserをインストールする必要があります。

Javaの並行性に対する2つのアプローチ

このExecutorServiceクラスはjava.util.concurrent、Java 5(およびもちろんJava 6の一部)で導入された革命の一部であり、Javaプラットフォームでのスレッド処理を簡素化しました。ExecutorServiceは、非同期タスクの進行状況の追跡と終了を管理するメソッドを提供するエグゼキュータです。が導入される前はjava.util.concurrent、Java開発者はサードパーティのライブラリに依存するか、プログラムの同時実行性を管理するために独自のクラスを作成していました。

Java7で導入されたFork / Joinは、既存の並行性ユーティリティクラスを置き換えたり競合したりすることを目的としたものではありません。代わりに、それらを更新して完了します。Fork / Joinは、Javaプログラムでの分割統治法または再帰的タスク処理の必要性に対応します(「参考文献」を参照)。

Fork / Joinのロジックは非常に単純です。(1)各大きなタスクを小さなタスクに分割(フォーク)します。(2)各タスクを別々のスレッドで処理します(必要に応じて、それらをさらに小さなタスクに分割します)。(3)結果を結合します。

続く2つのWebクローラの実装は、Java 6の特徴と機能実証シンプルなプログラムですExecutorServiceし、Java 7をForkJoinPool

Webクローラーの構築とベンチマーク

私たちのWebクローラーのタスクは、リンクを見つけて追跡することです。その目的は、リンクの検証である場合もあれば、データの収集である場合もあります。(たとえば、アンジェリーナ・ジョリーまたはブラッド・ピットの写真をWebで検索するようにプログラムに指示することができます。)

アプリケーションアーキテクチャは、次のもので構成されています。

  1. リンクと対話するための基本的な操作を公開するインターフェース。つまり、訪問したリンクの数を取得し、キューに訪問する新しいリンクを追加し、リンクを訪問済みとしてマークします
  2. アプリケーションの開始点にもなるこのインターフェイスの実装
  3. リンクがすでにアクセスされているかどうかを確認するためのビジネスロジックを保持するスレッド/再帰アクション。そうでない場合は、対応するページのすべてのリンクを収集し、新しいスレッド/再帰タスクを作成して、ExecutorServiceまたはに送信します。ForkJoinPool
  4. ExecutorServiceまたはForkJoinPoolタスクを待っているハンドルに

対応するページのすべてのリンクが返された後、リンクは「アクセス済み」と見なされることに注意してください。

Java6とJava7で利用可能な同時実行ツールを使用した開発の容易さを比較することに加えて、2つのベンチマークに基づいてアプリケーションのパフォーマンスを比較します。

  • 検索範囲:1,500の異なるリンクにアクセスするのに必要な時間を測定します
  • 処理能力:3,000の異なるリンクにアクセスするのに必要な時間を秒単位で測定します。これは、インターネット接続が処理する1秒あたりのキロビット数を測定するようなものです。

これらのベンチマークは比較的単純ですが、特定のアプリケーション要件について、Java6とJava7のJava同時実行のパフォーマンスについて少なくとも小さなウィンドウを提供します。

ExecutorServiceで構築されたJava6Webクローラー

Java 6 Webクローラーの実装では、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ます。

次に、LinkHandlerURLと対話するためのヘルパーメソッドを公開するインターフェイスを作成します。要件は次のとおりです。(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を備えたJava7Webクローラー

Java7で導入されたFork / Joinフレームワークは、実際には分割統治アルゴリズムの実装であり(「参考文献」を参照)、セントラルForkJoinPoolが分岐を実行しForkJoinTaskます。この例ではForkJoinPool、64スレッドで「バックアップ」されたものを使用します。sはスレッドよりも軽いので、私は支持されていると言いForkJoinTaskます。Fork / Joinでは、少数のスレッドで多数のタスクをホストできます。

Java 6の実装と同様に、64スレッドに基づくオブジェクトをWebCrawler7コンストラクターでインスタンス化することから始めますForkJoinPool

リスト4.Java 7LinkHandlerの実装

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のJava6実装とほぼ同じであることに注意してくださいqueueLink()。メソッドが欠落しているだけです。注目すべき最も重要なメソッドは、コンストラクターとstartCrawling()メソッドです。コンストラクターではForkJoinPool、64スレッドに裏打ちされた新しいものを作成します。(ForkJoinPoolJavadocではスレッド数は2の累乗でなければならないと記載されているため、50またはその他の丸め数ではなく64スレッドを選択しました。)プールはnewを呼び出し、LinkFinderActionさらにを再帰的に呼び出しForkJoinTasksます。リスト5はLinkFinderActionクラスを示しています: