Tip Java: Kapan menggunakan ForkJoinPool vs ExecutorService

Pustaka Fork / Join yang diperkenalkan di Java 7 memperluas paket konkurensi Java yang ada dengan dukungan untuk paralelisme perangkat keras, fitur utama dari sistem multi inti. Dalam Tip Java ini Madalin Ilie mendemonstrasikan dampak performa dari penggantian kelas Java 6 ExecutorServicedengan Java 7 ForkJoinPooldalam aplikasi perayap web.

Perayap web, juga dikenal sebagai laba-laba web, adalah kunci keberhasilan mesin telusur. Program ini terus-menerus memindai web, mengumpulkan jutaan halaman data dan mengirimkannya kembali ke database mesin pencari. Data tersebut kemudian diindeks dan diproses secara algoritme, menghasilkan hasil penelusuran yang lebih cepat dan lebih akurat. Meskipun paling terkenal digunakan untuk pengoptimalan penelusuran, perayap web juga dapat digunakan untuk tugas otomatis seperti validasi tautan atau menemukan dan mengembalikan data tertentu (seperti alamat email) dalam kumpulan halaman web.

Secara arsitektur, sebagian besar perayap web adalah program multithread berkinerja tinggi, meskipun dengan fungsionalitas dan persyaratan yang relatif sederhana. Oleh karena itu, membuat web crawler merupakan cara yang menarik untuk berlatih, serta membandingkan, teknik pemrograman multithread, atau bersamaan.

Kembalinya Java Tips!

Tips Java adalah artikel pendek berbasis kode yang mengundang pembaca JavaWorld untuk berbagi keterampilan dan penemuan pemrograman mereka. Beri tahu kami jika Anda memiliki tip untuk dibagikan dengan komunitas JavaWorld. Lihat juga Arsip Tip Java untuk tip pemrograman lainnya dari rekan-rekan Anda.

Dalam artikel ini saya akan membahas dua pendekatan untuk menulis web crawler: satu menggunakan Java 6 ExecutorService, dan yang lainnya ForkJoinPool Java 7. Untuk mengikuti contoh, Anda harus (saat tulisan ini dibuat) Java 7 update 2 diinstal di lingkungan pengembangan Anda, serta HtmlParser pustaka pihak ketiga.

Dua pendekatan untuk konkurensi Java

The ExecutorServicekelas merupakan bagian dari java.util.concurrentrevolusi diperkenalkan di Jawa 5 (dan sebagian Jawa 6, tentu saja), yang disederhanakan benang-penanganan pada platform Java. ExecutorServiceadalah Pelaksana yang menyediakan metode untuk mengelola pelacakan kemajuan dan penghentian tugas asinkron. Sebelum pengenalan java.util.concurrent, pengembang Java mengandalkan pustaka pihak ketiga atau menulis kelas mereka sendiri untuk mengelola konkurensi dalam program mereka.

Fork / Join, diperkenalkan di Java 7, tidak dimaksudkan untuk menggantikan atau bersaing dengan kelas utilitas konkurensi yang ada; alih-alih memperbarui dan menyelesaikannya. Fork / Join membahas kebutuhan untuk divide-and-conquer, atau pemrosesan tugas rekursif dalam program Java (lihat Sumberdaya).

Logika Fork / Join sangat sederhana: (1) pisahkan (garpu) setiap tugas besar menjadi tugas-tugas yang lebih kecil; (2) memproses setiap tugas dalam utas terpisah (memisahkannya menjadi tugas yang lebih kecil jika perlu); (3) gabungkan hasilnya.

Dua implementasi perayap web berikut adalah program sederhana yang mendemonstrasikan fitur dan fungsionalitas Java 6 ExecutorServicedan Java 7 ForkJoinPool.

Membangun dan mengukur crawler web

Tugas perayap web kami adalah menemukan dan mengikuti tautan. Tujuannya bisa untuk validasi link, atau bisa juga untuk mengumpulkan data. (Anda mungkin, misalnya, menginstruksikan program untuk menelusuri web untuk gambar Angelina Jolie, atau Brad Pitt.)

Arsitektur aplikasi terdiri dari:

  1. Antarmuka yang memperlihatkan operasi dasar untuk berinteraksi dengan tautan; yaitu mendapatkan jumlah link yang dikunjungi, menambahkan link baru untuk dikunjungi dalam antrian, menandai link sebagai telah dikunjungi
  2. Implementasi untuk antarmuka ini yang juga akan menjadi titik awal aplikasi
  3. Tindakan utas / rekursif yang akan menahan logika bisnis untuk memeriksa apakah tautan telah dikunjungi. Jika tidak, itu akan mengumpulkan semua tautan di halaman yang sesuai, membuat utas baru / tugas rekursif, dan mengirimkannya ke ExecutorServiceatauForkJoinPool
  4. Sebuah ExecutorServiceatau ForkJoinPooluntuk menangani tugas menunggu

Perhatikan bahwa link dianggap "dikunjungi" setelah semua link di halaman terkait dikembalikan.

Selain membandingkan kemudahan pengembangan menggunakan alat konkurensi yang tersedia di Java 6 dan Java 7, kami akan membandingkan kinerja aplikasi berdasarkan dua tolok ukur:

  • Cakupan penelusuran : Mengukur waktu yang diperlukan untuk mengunjungi 1.500 tautan berbeda
  • Daya pemrosesan : Mengukur waktu dalam detik yang diperlukan untuk mengunjungi 3.000 tautan tidak berbeda ; Ini seperti mengukur berapa kilobit per detik proses koneksi Internet Anda.

Meskipun relatif sederhana, tolok ukur ini akan memberikan setidaknya jendela kecil ke dalam kinerja konkurensi Java di Java 6 versus Java 7 untuk persyaratan aplikasi tertentu.

Crawler web Java 6 yang dibuat dengan ExecutorService

Untuk implementasi perayap web Java 6, kami akan menggunakan kumpulan utas tetap dari 64 utas, yang kami buat dengan memanggil Executors.newFixedThreadPool(int)metode pabrik. Kode 1 menunjukkan implementasi kelas utama.

Kode 1. Membangun 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(); } }

Dalam WebCrawler6konstruktor di atas , kami membuat kumpulan utas ukuran tetap dari 64 utas. Kami kemudian memulai program dengan memanggil startCrawlingmetode, yang membuat utas pertama dan mengirimkannya ke ExecutorService.

Selanjutnya, kami membuat LinkHandlerantarmuka, yang memperlihatkan metode pembantu untuk berinteraksi dengan URL. Persyaratannya adalah sebagai berikut: (1) menandai URL sebagai telah dikunjungi menggunakan addVisited()metode; (2) dapatkan jumlah URL yang dikunjungi melalui size()metode; (3) menentukan apakah URL telah dikunjungi dengan menggunakan visited()metode; dan (4) menambahkan URL baru dalam antrian melalui queueLink()metode.

Kode 2. Antarmuka 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); }

Sekarang, saat kita menjelajah halaman, kita perlu memulai sisa utas, yang kita lakukan melalui LinkFinderantarmuka, seperti yang ditunjukkan pada Daftar 3. Perhatikan linkHandler.queueLink(l)barisnya.

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

Logikanya LinkFindersederhana: (1) kita mulai mem-parsing URL; (2) setelah kami mengumpulkan semua tautan di dalam halaman terkait, kami menandai halaman tersebut sebagai telah dikunjungi; dan (3) kami mengirim setiap link yang ditemukan ke antrian dengan memanggil queueLink()metode tersebut. Metode ini sebenarnya akan membuat utas baru dan mengirimkannya ke ExecutorService. Jika utas "gratis" tersedia di kumpulan, utas akan dieksekusi; jika tidak maka akan ditempatkan dalam antrian tunggu. Setelah kami mencapai 1.500 tautan berbeda yang dikunjungi, kami mencetak statistik dan program terus berjalan.

Penjelajah web Java 7 dengan ForkJoinPool

Kerangka kerja Fork / Join yang diperkenalkan di Java 7 sebenarnya adalah implementasi dari algoritma Divide and Conquer (lihat Sumberdaya), di mana sebuah pusat ForkJoinPoolmenjalankan percabangan ForkJoinTask. Untuk contoh ini kita akan menggunakan ForkJoinPool"didukung" oleh 64 utas. Saya katakan didukung karena ForkJoinTasks lebih ringan dari pada utas. Di Fork / Join, sejumlah besar tugas dapat dihosting oleh jumlah thread yang lebih kecil.

Mirip dengan implementasi Java 6, kita mulai dengan membuat instance dalam WebCrawler7konstruktor sebuah ForkJoinPoolobjek yang didukung oleh 64 thread.

Kode 4. Java 7 implementasi 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(); } }

Perhatikan bahwa LinkHandlerantarmuka pada Listing 4 hampir sama dengan implementasi Java 6 dari Listing 2. Hanya kehilangan queueLink()metodenya. Metode yang paling penting untuk dilihat adalah konstruktor dan startCrawling()metode. Di konstruktor, kami membuat yang baru ForkJoinPooldidukung oleh 64 utas. (Saya telah memilih 64 utas alih-alih 50 atau bilangan bulat lainnya karena di ForkJoinPoolJavadoc itu menyatakan bahwa jumlah utas harus menjadi pangkat dua.) Kumpulan memanggil baru LinkFinderAction, yang akan secara rekursif memanggil lebih lanjut ForkJoinTasks. Kode 5 menunjukkan LinkFinderActionkelas: