Προγραμματισμός

Συμβουλή Java: Πότε να χρησιμοποιήσετε το ForkJoinPool εναντίον ExecutorService

Η βιβλιοθήκη Fork / Join που εισήχθη στο Java 7 επεκτείνει το υπάρχον πακέτο ταυτόχρονης Java με υποστήριξη για παραλληλισμό υλικού, βασικό χαρακτηριστικό των συστημάτων πολλαπλών πυρήνων. Σε αυτήν την Συμβουλή Java Ο Madalin Ilie καταδεικνύει τον αντίκτυπο στην απόδοση της αντικατάστασης του Java 6 Εξυπηρέτηση τάξη με Java 7's ForkJoinPool σε μια εφαρμογή ανίχνευσης ιστού.

Οι ανιχνευτές ιστού, επίσης γνωστοί ως αράχνες ιστού, είναι το κλειδί για την επιτυχία των μηχανών αναζήτησης. Αυτά τα προγράμματα σαρώνουν διαρκώς τον Ιστό, συγκεντρώνοντας εκατομμύρια σελίδες δεδομένων και στέλνοντάς τα πίσω σε βάσεις δεδομένων μηχανών αναζήτησης. Στη συνέχεια, τα δεδομένα ευρετηριάζονται και υποβάλλονται σε επεξεργασία αλγοριθμικά, με αποτέλεσμα ταχύτερα, πιο ακριβή αποτελέσματα αναζήτησης. Ενώ χρησιμοποιούνται πιο διάσημα για βελτιστοποίηση αναζήτησης, τα προγράμματα ανίχνευσης ιστού μπορούν επίσης να χρησιμοποιηθούν για αυτοματοποιημένες εργασίες, όπως επικύρωση συνδέσμου ή εύρεση και επιστροφή συγκεκριμένων δεδομένων (όπως διευθύνσεις email) σε μια συλλογή ιστοσελίδων.

Αρχιτεκτονικά, τα περισσότερα προγράμματα ανίχνευσης ιστού είναι προγράμματα πολλαπλών νημάτων υψηλής απόδοσης, αν και με σχετικά απλές λειτουργίες και απαιτήσεις. Η δημιουργία ενός προγράμματος ανίχνευσης ιστού είναι επομένως ένας ενδιαφέρων τρόπος για να εξασκηθείτε, καθώς και να συγκρίνετε, πολυνηματικές ή ταυτόχρονες, τεχνικές προγραμματισμού.

Η επιστροφή των συμβουλών Java!

Οι συμβουλές Java είναι σύντομα άρθρα βάσει κώδικα που προσκαλούν τους αναγνώστες του JavaWorld να μοιραστούν τις δεξιότητες προγραμματισμού και τις ανακαλύψεις τους. Ενημερώστε μας εάν έχετε μια συμβουλή για κοινή χρήση με την κοινότητα JavaWorld. Δείτε επίσης το Java Tips Archive για περισσότερες συμβουλές προγραμματισμού από τους συνομηλίκους σας.

Σε αυτό το άρθρο θα ακολουθήσω δύο προσεγγίσεις για τη σύνταξη ενός προγράμματος ανίχνευσης ιστού: μία χρησιμοποιώντας το Java 6 ExecutorService και το άλλο Java 7's ForkJoinPool. Για να ακολουθήσετε τα παραδείγματα, θα πρέπει να έχετε (από αυτήν τη γραφή) την ενημερωμένη έκδοση Java 7 εγκατεστημένη στο περιβάλλον ανάπτυξης, καθώς και τη βιβλιοθήκη τρίτων κατασκευαστών HtmlParser.

Δύο προσεγγίσεις για την ταυτόχρονη Java

ο Εξυπηρέτηση τάξη είναι μέρος του java.util.concurrent Η επανάσταση εισήχθη στο Java 5 (και μέρος της Java 6, φυσικά), η οποία απλοποίησε το χειρισμό νήματος στην πλατφόρμα Java. Εξυπηρέτηση είναι ένας εκτελεστής που παρέχει μεθόδους για τη διαχείριση της παρακολούθησης προόδου και τον τερματισμό των ασύγχρονων εργασιών. Πριν από την εισαγωγή του java.util.concurrent, Οι προγραμματιστές Java βασίστηκαν σε βιβλιοθήκες τρίτων ή έγραψαν τα δικά τους μαθήματα για να διαχειριστούν την ταυτόχρονη χρήση των προγραμμάτων τους.

Το Fork / Join, που εισήχθη στο Java 7, δεν προορίζεται να αντικαταστήσει ή να ανταγωνιστεί τις υπάρχουσες κλάσεις βοηθητικών προγραμμάτων. Αντ 'αυτού ενημερώνει και τα ολοκληρώνει. Το Fork / Join αντιμετωπίζει την ανάγκη για διαίρεση και κατάκτηση, ή αναδρομική επεξεργασία εργασιών σε προγράμματα Java (βλ. πόρους).

Η λογική του Fork / Join είναι πολύ απλή: (1) χωρίστε (πιρούνι) κάθε μεγάλη εργασία σε μικρότερες εργασίες. (2) επεξεργασία κάθε εργασίας σε ξεχωριστό νήμα (διαχωρίζοντας αυτές σε ακόμη μικρότερες εργασίες, εάν είναι απαραίτητο). (3) συμμετάσχετε στα αποτελέσματα.

Οι δύο υλοποιήσεις του προγράμματος ανίχνευσης ιστού που ακολουθούν είναι απλά προγράμματα που αποδεικνύουν τις δυνατότητες και τη λειτουργικότητα του Java 6 Εξυπηρέτηση και το Java 7 ForkJoinPool.

Δημιουργία και συγκριτική αξιολόγηση του προγράμματος ανίχνευσης ιστού

Ο στόχος του προγράμματος ανίχνευσης ιστού μας είναι να βρούμε και να ακολουθούμε συνδέσμους. Ο σκοπός του θα μπορούσε να είναι επικύρωση συνδέσμου ή θα μπορούσε να συλλέγει δεδομένα. (Μπορείτε, για παράδειγμα, να δώσετε εντολή στο πρόγραμμα να πραγματοποιήσει αναζήτηση στον ιστό για φωτογραφίες της Angelina Jolie ή του Brad Pitt.)

Η αρχιτεκτονική της εφαρμογής αποτελείται από τα ακόλουθα:

  1. Μια διεπαφή που εκθέτει βασικές λειτουργίες για αλληλεπίδραση με συνδέσμους. δηλ. λάβετε τον αριθμό των συνδέσμων που επισκέπτεστε, προσθέστε νέους συνδέσμους που θα επισκεφθείτε στην ουρά, σημειώστε έναν σύνδεσμο ως επισκέφθηκε
  2. Μια εφαρμογή για αυτήν τη διεπαφή που θα είναι επίσης το σημείο εκκίνησης της εφαρμογής
  3. Ένα νήμα / αναδρομική ενέργεια που θα κρατήσει τη λογική της επιχείρησης για να ελέγξει εάν έχει ήδη επισκεφτεί έναν σύνδεσμο. Εάν όχι, θα συγκεντρώσει όλους τους συνδέσμους στην αντίστοιχη σελίδα, θα δημιουργήσει ένα νέο νήμα / αναδρομική εργασία και θα το υποβάλει στο Εξυπηρέτηση ή ForkJoinPool
  4. Ενα Εξυπηρέτηση ή ForkJoinPool για να χειριστείτε τις εργασίες αναμονής

Λάβετε υπόψη ότι ένας σύνδεσμος θεωρείται "επισκέφθηκε" μετά την επιστροφή όλων των συνδέσμων στην αντίστοιχη σελίδα.

Εκτός από τη σύγκριση της ευκολίας ανάπτυξης χρησιμοποιώντας τα εργαλεία ταυτόχρονης πρόσβασης που είναι διαθέσιμα στα Java 6 και Java 7, θα συγκρίνουμε την απόδοση της εφαρμογής με βάση δύο σημεία αναφοράς:

  • Κάλυψη αναζήτησης: Μετρά τον απαιτούμενο χρόνο για επίσκεψη 1.500 διακριτή συνδέσεις
  • Επεξεργαστικη ΙΣΧΥΣ: Μετρά τον χρόνο σε δευτερόλεπτα που απαιτείται για να επισκεφτείτε 3.000 μη διακριτός συνδέσεις; Αυτό είναι σαν να μετράτε πόσα kilobits ανά δευτερόλεπτο οι διαδικασίες σύνδεσής σας στο Διαδίκτυο.

Αν και είναι σχετικά απλά, αυτά τα σημεία αναφοράς θα παρέχουν τουλάχιστον ένα μικρό παράθυρο στην απόδοση της ταυτόχρονης Java στην Java 6 έναντι της Java 7 για ορισμένες απαιτήσεις εφαρμογής.

Ένα πρόγραμμα ανίχνευσης ιστού Java 6 που δημιουργήθηκε με το ExecutorService

Για την υλοποίηση του Java 6 web crawler θα χρησιμοποιήσουμε ένα σύνολο 64 νημάτων, το οποίο δημιουργούμε καλώντας το Executors.newFixedThreadPool (int) εργοστασιακή μέθοδος. Η λίστα 1 δείχνει την κύρια εφαρμογή τάξης.

Λίστα 1. Κατασκευή ενός WebCrawler

πακέτο insidecoding.webcrawler; εισαγωγή java.util.Collection; εισαγωγή java.util.Collections; εισαγωγή java.util.concurrent.ExecutorService; εισαγωγή java.util.concurrent.Executors; εισαγωγή insidecoding.webcrawler.net.LinkFinder; εισαγωγή java.util.HashSet; / ** * * @author Madalin Ilie * / δημόσια τάξη WebCrawler6 υλοποιεί το LinkHandler {private final Collection visitLinks = Collections.synchronizedSet (new HashSet ()); // ιδιωτικό τελικό Συλλογή visitLinks = Collections.synchronizedList (νέο ArrayList ()); ιδιωτική διεύθυνση URL συμβολοσειράς; ιδιωτικό ExecutorService execService; δημόσιο WebCrawler6 (String startURL, int maxThreads) {this.url = startURL; execService = Executors.newFixedThreadPool (maxThreads); } @Override public void queueLink (String link) ρίχνει την εξαίρεση {startNewThread (link); } @Override δημόσιο int μέγεθος () {return visitLinks.size (); } @Override public void addVisited (String s) {VisitLinks.add (s); } @Override δημόσια boolean που επισκέφτηκαν (String s) {return دوروLinks.contains (s); } private void startNewThread (String link) ρίχνει Exception {execService.execute (νέο LinkFinder (σύνδεσμος, αυτό)); } το ιδιωτικό κενό startCrawling () ρίχνει την Εξαίρεση {startNewThread (this.url); } / ** * @param υποστηρίζει τα ορίσματα γραμμής εντολών * / public static void main (String [] args) ρίχνει την Εξαίρεση {new WebCrawler ("// www.javaworld.com", 64) .startCrawling (); }}

Στα παραπάνω WebCrawler6 κατασκευαστής, δημιουργούμε μια ομάδα νήματος σταθερού μεγέθους 64 νημάτων. Στη συνέχεια ξεκινάμε το πρόγραμμα καλώντας το startCrawling μέθοδος, η οποία δημιουργεί το πρώτο νήμα και το υποβάλλει στο Εξυπηρέτηση.

Στη συνέχεια, δημιουργούμε ένα LinkHandler διεπαφή, η οποία εκθέτει βοηθητικές μεθόδους αλληλεπίδρασης με διευθύνσεις URL. Οι απαιτήσεις έχουν ως εξής: (1) επισημάνετε μια διεύθυνση URL ως επισκέφθηκε χρησιμοποιώντας το addVisited () μέθοδος; (2) λάβετε τον αριθμό των διευθύνσεων URL που επισκεφτήκατε μέσω του Μέγεθος() μέθοδος; (3) προσδιορίστε εάν έχει ήδη επισκεφτεί μια διεύθυνση URL χρησιμοποιώντας το επισκέφτηκε () μέθοδος; και (4) προσθέστε ένα νέο URL στην ουρά μέσω του queueLink () μέθοδος.

Λίστα 2. Η διασύνδεση LinkHandler

πακέτο insidecoding.webcrawler; / ** * * @author Madalin Ilie * / δημόσια διεπαφή LinkHandler {/ ** * Τοποθετεί τον σύνδεσμο στην ουρά * @param link * @throws Exception * / void queueLink (String link) ρίχνει την Εξαίρεση; / ** * Επιστρέφει τον αριθμό των επισκεπτόμενων συνδέσμων * @return * / int size (); / ** * Ελέγχει εάν ο σύνδεσμος έχει ήδη επισκεφτεί * @param link * @return * / boolean επισκέψεων (String link); / ** * Επισημαίνει αυτόν το σύνδεσμο ως επισκέφθηκε * @param link * / void addVisited (String link); }

Τώρα, καθώς ανιχνεύουμε σελίδες, πρέπει να ξεκινήσουμε τα υπόλοιπα νήματα, τα οποία κάνουμε μέσω του LinkFinder διεπαφή, όπως φαίνεται στην καταχώριση 3. Σημειώστε το linkHandler.queueLink (l) γραμμή.

Λίστα 3. LinkFinder

πακέτο insidecoding.webcrawler.net; εισαγωγή java.net.URL; εισαγωγή org.htmlparser.Parser; εισαγωγή org.htmlparser.filters.NodeClassFilter; εισαγωγή org.htmlparser.tags.LinkTag; εισαγωγή org.htmlparser.util.NodeList; εισαγωγή insidecoding.webcrawler.LinkHandler; / ** * * @author Madalin Ilie * / Δημόσια τάξη LinkFinder υλοποιεί Runnable {private String url; ιδιωτικό LinkHandler linkHandler; / ** * Χρησιμοποιημένα στατιστικά στοιχεία fot * / private static final long t0 = System.nanoTime (); δημόσιο LinkFinder (String url, LinkHandler handler) {this.url = url; this.linkHandler = χειριστής; } @Override public void run () {getSimpleLinks (url); } private void getSimpleLinks (String url) {// εάν δεν το έχετε ήδη επισκεφτεί εάν (! linkHandler.visited (url)) {δοκιμάστε {URL uriLink = new URL (url); Parser parser = νέο Parser (uriLink.openConnection ()); Λίστα NodeList = parser.extractAllNodesThatMatch (νέο NodeClassFilter (LinkTag.class)); Λίστα url = νέο ArrayList (); για (int i = 0; i <list.size (); i ++) {LinkTag extracted = (LinkTag) list.elementAt (i); εάν (! extracted.getLink (). isEmpty () &&! linkHandler.visited (extracted.getLink ())) {urls.add (extracted.getLink ()); }} // επισκεφθήκαμε αυτό το url linkHandler.addVisited (url); if (linkHandler.size () == 1500) {System.out.println ("Ώρα να επισκεφθείτε 1500 ξεχωριστούς συνδέσμους =" + (System.nanoTime () - t0)) } για (String l: urls) {linkHandler.queueLink (l); }} catch (Εξαίρεση e) {// αγνοήστε όλα τα σφάλματα προς το παρόν}}}}

Η λογική του LinkFinder είναι απλό: (1) αρχίζουμε να αναλύουμε μια διεύθυνση URL. (2) αφού συγκεντρώσουμε όλους τους συνδέσμους στην αντίστοιχη σελίδα, επισημαίνουμε τη σελίδα ως επισκέφθηκε. και (3) στέλνουμε κάθε σύνδεσμο που βρέθηκε σε μια ουρά καλώντας το queueLink () μέθοδος. Αυτή η μέθοδος θα δημιουργήσει πραγματικά ένα νέο νήμα και θα το στείλει στο Εξυπηρέτηση. Εάν είναι διαθέσιμα "δωρεάν" νήματα στην ομάδα, το νήμα θα εκτελεστεί. Διαφορετικά θα τοποθετηθεί σε ουρά αναμονής. Αφού φτάσουμε σε 1.500 ξεχωριστούς συνδέσμους που επισκεφτήκαμε, εκτυπώνουμε τα στατιστικά στοιχεία και το πρόγραμμα συνεχίζει να εκτελείται.

Ένα πρόγραμμα ανίχνευσης ιστού Java 7 με το ForkJoinPool

Το πλαίσιο Fork / Join που εισήχθη στο Java 7 είναι στην πραγματικότητα μια εφαρμογή του αλγορίθμου Divide and Conquer (βλ. Πόρους), στον οποίο ForkJoinPool εκτελεί διακλάδωση ForkJoinTaskμικρό. Για αυτό το παράδειγμα θα χρησιμοποιήσουμε ένα ForkJoinPool "υποστηρίζεται" από 64 νήματα. λέω υποστηρίζεται επειδή ForkJoinTaskΕίναι ελαφρύτερα από τα νήματα. Στο Fork / Join, ένας μεγάλος αριθμός εργασιών μπορεί να φιλοξενηθεί από μικρότερο αριθμό νημάτων.

Παρόμοια με την εφαρμογή Java 6, ξεκινάμε με το instantiating στο WebCrawler7 κατασκευαστής α ForkJoinPool αντικείμενο που υποστηρίζεται από 64 νήματα.

Λίστα 4. Εφαρμογή Java 7 LinkHandler

πακέτο insidecoding.webcrawler7; εισαγωγή java.util.Collection; εισαγωγή java.util.Collections; εισαγωγή java.util.concurrent.ForkJoinPool; εισαγωγή insidecoding.webcrawler7.net.LinkFinderAction; εισαγωγή java.util.HashSet; / ** * * @author Madalin Ilie * / δημόσια τάξη WebCrawler7 υλοποιεί το LinkHandler {private final Collection visitLinks = Collections.synchronizedSet (new HashSet ()); // ιδιωτικό τελικό Συλλογή visitLinks = Collections.synchronizedList (νέο ArrayList ()); ιδιωτική διεύθυνση URL συμβολοσειράς; ιδιωτικό ForkJoinPool mainPool; δημόσιο WebCrawler7 (String startURL, int maxThreads) {this.url = startURL; mainPool = νέο ForkJoinPool (maxThreads); } private void startCrawling () {mainPool.invoke (νέο LinkFinderAction (this.url, this)); } @Override δημόσιο int μέγεθος () {return visitLinks.size (); } @Override public void addVisited (String s) {VisitLinks.add (s); } @Override δημόσια boolean που επισκέφτηκαν (String s) {return دوروLinks.contains (s); } / ** * @param υποστηρίζει τα ορίσματα γραμμής εντολών * / public static void main (String [] args) ρίχνει την Εξαίρεση {new WebCrawler7 ("// www.javaworld.com", 64) .startCrawling (); }}

Σημειώστε ότι το LinkHandler διεπαφή στην καταχώριση 4 είναι σχεδόν ίδια με την εφαρμογή Java 6 από την καταχώριση 2. Λείπει μόνο το queueLink () μέθοδος. Οι πιο σημαντικές μέθοδοι που πρέπει να εξετάσουμε είναι ο κατασκευαστής και το startCrawling () μέθοδος. Στον κατασκευαστή, δημιουργούμε ένα νέο ForkJoinPool υποστηρίζεται από 64 νήματα. (Έχω επιλέξει 64 νήματα αντί για 50 ή κάποιο άλλο στρογγυλό αριθμό επειδή στο ForkJoinPool Javadoc δηλώνει ότι ο αριθμός των νημάτων πρέπει να είναι δύναμη δύο.) Η ομάδα επικαλείται ένα νέο LinkFinderAction, το οποίο θα επικαλεστεί αναδρομικά περαιτέρω ForkJoinTasks. Η λίστα 5 δείχνει το LinkFinderAction τάξη: