Petua Java: Bila hendak menggunakan ForkJoinPool vs ExecutorService

Perpustakaan Fork / Join yang diperkenalkan di Java 7 memperluas pakej kesesuaian Java yang ada dengan sokongan untuk paralelisme perkakasan, ciri utama sistem multikore. Dalam Petua Java ini Madalin Ilie menunjukkan kesan prestasi penggantian kelas Java 6 ExecutorServicedengan Java 7 ForkJoinPooldalam aplikasi web crawler.

Perayap web, juga dikenali sebagai labah-labah web, adalah kunci kejayaan mesin pencari. Program-program ini sentiasa mengimbas web, mengumpulkan jutaan halaman data dan mengirimkannya kembali ke pangkalan data mesin pencari. Data kemudian diindeks dan diproses secara algoritma, menghasilkan hasil carian yang lebih cepat dan tepat. Walaupun mereka paling terkenal digunakan untuk pengoptimuman pencarian, perayap web juga dapat digunakan untuk tugas-tugas automatik seperti pengesahan pautan atau mencari dan mengembalikan data tertentu (seperti alamat e-mel) dalam koleksi halaman web.

Dari segi seni bina, kebanyakan perayap web adalah program multithreaded berprestasi tinggi, walaupun dengan fungsi dan keperluan yang agak mudah. Oleh itu, membina perayap web adalah kaedah menarik untuk mempraktikkan, serta membandingkan, memprogram, teknik multithreaded, atau serentak.

Kepulangan Petua Java!

Petua Java adalah artikel ringkas, berdasarkan kod yang mengundang pembaca JavaWorld untuk berkongsi kemahiran dan penemuan pengaturcaraan mereka. Beritahu kami jika anda mempunyai tip untuk berkongsi dengan komuniti JavaWorld. Lihat juga Arkib Petua Java untuk lebih banyak petua pengaturcaraan dari rakan sebaya anda.

Dalam artikel ini saya akan melalui dua pendekatan untuk menulis crawler web: satu menggunakan Java 6 ExecutorService, dan yang lain Java 7's ForkJoinPool. Untuk mengikuti contohnya, anda perlu memasang (dari penulisan ini) Java 7 kemas kini 2 di persekitaran pengembangan anda, serta perpustakaan pihak ketiga HtmlParser.

Dua pendekatan untuk serentak Java

The ExecutorServicekelas adalah sebahagian daripada java.util.concurrentrevolusi diperkenalkan di Jawa 5 (dan sebahagian daripada Java 6, sudah tentu), yang dipermudahkan thread pengendalian pada platform Jawa. ExecutorServiceadalah Pelaksana yang menyediakan kaedah untuk mengurus kemajuan dan penamatan tugas tak segerak. Sebelum pengenalan java.util.concurrent, pemaju Java bergantung pada perpustakaan pihak ketiga atau menulis kelas mereka sendiri untuk menguruskan kesesuaian dalam program mereka.

Fork / Join, diperkenalkan di Java 7, tidak bertujuan untuk menggantikan atau bersaing dengan kelas utiliti serentak yang ada; sebaliknya ia mengemas kini dan melengkapkannya. Fork / Join menangani keperluan pemecahan dan penaklukan, atau pemprosesan tugas berulang dalam program Java (lihat Sumber).

Logik Fork / Join sangat mudah: (1) asingkan (garpu) setiap tugas besar menjadi tugas yang lebih kecil; (2) memproses setiap tugas dalam utas yang terpisah (memisahkan tugas tersebut menjadi tugas yang lebih kecil jika perlu); (3) sertai hasilnya.

Dua implementasi web crawler yang diikuti adalah program sederhana yang menunjukkan ciri dan fungsi Java 6 ExecutorServicedan Java 7 ForkJoinPool.

Membangun dan menanda aras perayap web

Tugas perayap web kami adalah mencari dan mengikuti pautan. Tujuannya adalah pengesahan tautan, atau pengumpulan data. (Anda mungkin, misalnya, mengarahkan program untuk mencari di web gambar Angelina Jolie, atau Brad Pitt.)

Seni bina aplikasi terdiri daripada yang berikut:

  1. Antara muka yang memperlihatkan operasi asas untuk berinteraksi dengan pautan; iaitu, dapatkan jumlah pautan yang dikunjungi, tambahkan pautan baru untuk dikunjungi dalam barisan, tandakan pautan sebagai dikunjungi
  2. Pelaksanaan untuk antara muka ini yang juga akan menjadi titik permulaan aplikasi
  3. Urutan / tindakan rekursif yang akan menahan logik perniagaan untuk memeriksa sama ada pautan telah dikunjungi. Sekiranya tidak, ia akan mengumpulkan semua pautan di halaman yang sesuai, membuat utas baru / tugas berulang, dan menyerahkannya ke ExecutorServiceatauForkJoinPool
  4. Satu ExecutorServiceatau ForkJoinPooluntuk menangani tugas menunggu

Perhatikan bahawa pautan dianggap "dikunjungi" setelah semua pautan di halaman yang sesuai dikembalikan.

Selain membandingkan kemudahan pengembangan menggunakan alat konkurensi yang tersedia di Java 6 dan Java 7, kami akan membandingkan prestasi aplikasi berdasarkan dua tanda aras:

  • Liputan carian : Mengukur masa yang diperlukan untuk mengunjungi 1,500 pautan berbeza
  • Kuasa pemprosesan : Mengukur masa dalam beberapa saat yang diperlukan untuk melawat 3,000 pautan yang tidak berbeza ; ini seperti mengukur berapa kilobit sesaat proses sambungan Internet anda.

Walaupun relatif sederhana, penanda aras ini akan memberikan sekurang-kurangnya jendela kecil ke arah kinerja persamaan Java di Java 6 berbanding Java 7 untuk keperluan aplikasi tertentu.

Perangkak web Java 6 yang dibina dengan ExecutorService

Untuk implementasi Java 6 crawler, kami akan menggunakan kumpulan utas tetap 64 utas, yang kami buat dengan memanggil Executors.newFixedThreadPool(int)kaedah kilang. Penyenaraian 1 menunjukkan pelaksanaan kelas utama.

Penyenaraian 1. Membina 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 benang bersaiz tetap sebanyak 64 utas. Kami kemudian memulakan program dengan memanggil startCrawlingkaedah, yang membuat utas pertama dan menyerahkannya ke ExecutorService.

Seterusnya, kami membuat LinkHandlerantara muka, yang memperlihatkan kaedah pembantu untuk berinteraksi dengan URL. Syaratnya adalah seperti berikut: (1) tandakan URL sebagai dikunjungi menggunakan addVisited()kaedah; (2) dapatkan jumlah URL yang dikunjungi melalui size()kaedah; (3) menentukan sama ada URL telah dikunjungi menggunakan visited()kaedah; dan (4) tambahkan URL baru dalam barisan melalui queueLink()kaedah.

Penyenaraian 2. Antara muka 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, ketika kita merangkak halaman, kita perlu memulai sisa utas, yang kita lakukan melalui LinkFinderantara muka, seperti yang ditunjukkan dalam Penyenaraian 3. Perhatikan linkHandler.queueLink(l)barisnya.

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

Logiknya LinkFindermudah: (1) kita mula menghuraikan URL; (2) setelah kami mengumpulkan semua pautan dalam halaman yang sesuai, kami menandakan halaman tersebut sebagai dilawati; dan (3) kami menghantar setiap pautan yang dijumpai ke barisan dengan memanggil queueLink()kaedah. Kaedah ini sebenarnya akan membuat utas baru dan menghantarnya ke ExecutorService. Sekiranya benang "percuma" tersedia di kolam, utas akan dilaksanakan; jika tidak, ia akan diletakkan dalam barisan menunggu. Setelah mencapai 1,500 pautan berbeza yang dikunjungi, kami mencetak statistik dan program ini terus berjalan.

Perangkak web Java 7 dengan ForkJoinPool

Kerangka Fork / Join yang diperkenalkan di Java 7 sebenarnya adalah implementasi dari algoritma Divide and Conquer (lihat Sumber), di mana pusat ForkJoinPoolmelaksanakan percabangan ForkJoinTasks. Untuk contoh ini kita akan menggunakan ForkJoinPool"disokong" oleh 64 utas. Saya katakan disokong kerana ForkJoinTasks lebih ringan daripada utas. Dalam Fork / Join, sebilangan besar tugas dapat dihoskan oleh sebilangan kecil utas.

Sama seperti implementasi Java 6, kita mulai dengan memberi contoh pada WebCrawler7konstruktor ForkJoinPoolobjek yang disokong oleh 64 utas.

Penyenaraian 4. Java 7 LinkHandler pelaksanaan

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 bahawa LinkHandlerantara muka dalam Penyenaraian 4 hampir sama dengan pelaksanaan Java 6 dari Penyenaraian 2. Hanya queueLink()kaedah yang hilang . Kaedah yang paling penting untuk dilihat adalah konstruktor dan startCrawling()kaedahnya. Dalam konstruktor, kami membuat yang baru ForkJoinPooldisokong oleh 64 utas. (Saya telah memilih 64 utas dan bukannya 50 atau beberapa nombor bulat yang lain kerana di ForkJoinPoolJavadoc menyatakan bahawa bilangan utas mesti mempunyai kekuatan dua.) Kolam memanggil baru LinkFinderAction, yang secara berulang akan memanggil lebih jauh ForkJoinTasks. Penyenaraian 5 menunjukkan LinkFinderActionkelas: