簡體   English   中英

ExecutorService永遠不會停止。在另一個執行任務中執行新任務時

[英]ExecutorService never stops. When execute new task inside another executing task

美好的一天。

我的網絡抓取工具有阻塞問題。 邏輯很簡單。 首先創建一個Runnable ,它下載html文檔,掃描所有鏈接,然后在所有資助的鏈接上創建新的Runnable對象。 每個新創建的Runnable都會為每個鏈接創建新的Runnable對象並執行它們。

問題是ExecutorService永遠不會停止。

CrawlerTest.java

public class CrawlerTest {

    public static void main(String[] args) throws InterruptedException {
        new CrawlerService().crawlInternetResource("https://jsoup.org/");
    }
}

CrawlerService.java

import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class CrawlerService {

    private Set<String> uniqueUrls = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(10000));
    private ExecutorService executorService = Executors.newFixedThreadPool(8);
    private String baseDomainUrl;

    public void crawlInternetResource(String baseDomainUrl) throws InterruptedException {
        this.baseDomainUrl = baseDomainUrl;
        System.out.println("Start");
        executorService.execute(new Crawler(baseDomainUrl)); //Run first thread and scan main domain page. This thread produce new threads.
        executorService.awaitTermination(10, TimeUnit.MINUTES);
        System.out.println("End");
    }

    private class Crawler implements Runnable { // Inner class that encapsulates thread and scan for links

        private String urlToCrawl;

        public Crawler(String urlToCrawl) {
            this.urlToCrawl = urlToCrawl;
        }

        public void run() {
            try {
                findAllLinks();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void findAllLinks() throws InterruptedException {
            /*Try to add new url in collection, if url is unique adds it to collection, 
             * scan document and start new thread for finded links*/
            if (uniqueUrls.add(urlToCrawl)) { 
                System.out.println(urlToCrawl);

                Document htmlDocument = loadHtmlDocument(urlToCrawl);
                Elements findedLinks = htmlDocument.select("a[href]");

                for (Element link : findedLinks) {
                    String absLink = link.attr("abs:href");
                    if (absLink.contains(baseDomainUrl) && !absLink.contains("#")) { //Check that we are don't go out of domain
                        executorService.execute(new Crawler(absLink)); //Start new thread for each funded link
                    }
                }
            }
        }

        private Document loadHtmlDocument(String internetResourceUrl) {
            Document document = null;
            try {
                document = Jsoup.connect(internetResourceUrl).ignoreHttpErrors(true).ignoreContentType(true)
                        .userAgent("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:48.0) Gecko/20100101 Firefox/48.0")
                        .timeout(10000).get();
            } catch (IOException e) {
                System.out.println("Page load error");
                e.printStackTrace();
            }
            return document;
        }
    }
}

此應用程序需要大約20秒來掃描jsoup.org以獲取所有唯一鏈接。 但它只需等待10分鍾executorService.awaitTermination(10, TimeUnit.MINUTES); 然后我看到死主線程仍在工作執行器。

主題

如何強制ExecutorService正常工作?

我認為問題是它在主線程中調用另一個任務中的executorService.execute。

你是在濫用awaitTermination 根據javadoc你應該首先調用shutdown

阻止所有任務在關閉請求之后完成執行,或者發生超時,或者當前線程被中斷,以先發生者為准。

為了實現你的目標,我建議使用CountDownLatch (或支持像這樣的增量的鎖存器)來確定沒有任務的確切時刻,這樣你就可以安全地shutdown

我從前面看到你的評論:

我不能使用CountDownLatch,因為我事先不知道我將從資源中收集多少個唯一鏈接。

首先,vsminkov就是為什么awaitTermniation將等待10分鍾的答案。 我會提供另一種解決方案。

而不是使用CountDownLatch使用Phaser 對於每個新任務,您都可以注冊並等待完成。

創建一個移相器並在每次調用execute.submit register並在每次Runnable完成時arrive

public void crawlInternetResource(String baseDomainUrl) {
    this.baseDomainUrl = baseDomainUrl;

    Phaser phaser = new Phaser();
    executorService.execute(new Crawler(phaser, baseDomainUrl)); 
    int phase = phaser.getPhase();
    phase.awaitAdvance(phase);
}

private class Crawler implements Runnable { 

    private final Phaser phaser;
    private String urlToCrawl;

    public Crawler(Phaser phaser, String urlToCrawl) {
        this.urlToCrawl = urlToCrawl;
        this.phaser = phaser;
        phaser.register(); // register new task
    }

    public void run(){
       ...
       phaser.arrive(); //may want to surround this in try/finally
    }

你不是在叫停機。

這可能有效--CrawlerService中的AtomicLong變量。 在將每個新子任務提交給執行程序服務之前遞增。

修改run()方法以遞減此計數器,如果為0,則關閉執行程序服務

public void run() {
    try {
        findAllLinks();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        //decrements counter
        //If 0, shutdown executor from here or just notify CrawlerService who would be doing wait().
    }
}

在“finally”中,減少計數器,當計數器為零時,關閉執行程序或只是通知CrawlerService。 0表示,這是最后一個,沒有其他正在運行,沒有在隊列中掛起。 沒有任務會提交任何新的子任務。

如何強制ExecutorService正常工作?

我認為問題是它在主線程中調用另一個任務中的executorService.execute。

不。問題不在於ExecutorService。 您使用的API方式不正確,因此無法獲得正確的結果。

您必須按特定順序使用三個API才能獲得正確的結果。

1. shutdown
2. awaitTermination
3. shutdownNow

ExecutorService的 oracle文檔頁面推薦的方法:

 void shutdownAndAwaitTermination(ExecutorService pool) {
   pool.shutdown(); // Disable new tasks from being submitted
   try {
     // Wait a while for existing tasks to terminate
     if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
       pool.shutdownNow(); // Cancel currently executing tasks
       // Wait a while for tasks to respond to being cancelled
       if (!pool.awaitTermination(60, TimeUnit.SECONDS))
           System.err.println("Pool did not terminate");
     }
   } catch (InterruptedException ie) {
     // (Re-)Cancel if current thread also interrupted
     pool.shutdownNow();
     // Preserve interrupt status
     Thread.currentThread().interrupt();
   }

shutdown():啟動有序關閉,其中執行先前提交的任務,但不接受任何新任務。

shutdownNow():嘗試停止所有正在執行的任務,停止等待任務的處理,並返回等待執行的任務列表。

awaitTermination():阻塞,直到所有任務在關閉請求完成后執行,或發生超時,或者當前線程被中斷,以先發生者為准。

另請注意:如果要等待所有任務完成,請參閱此相關的SE問題:

等到所有線程完成他們在java中的工作

我更喜歡使用invokeAll()ForkJoinPool() ,它們最適合您的用例。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM