美好的一天。
我的网络抓取工具项目存在拦截问题。
逻辑很简单。首先创建一个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。
答案 0 :(得分:3)
您滥用awaitTermination
。根据javadoc,您应首先致电shutdown
:
阻止所有任务在关闭请求之后完成执行,或发生超时,或者当前线程被中断,以先发生者为准。
为了实现您的目标,我建议您使用CountDownLatch
(或锁定支持增量,如this one)来确定没有任务的确切时刻,这样您就可以安全地{{1} }。
答案 1 :(得分:2)
我在前面看到你的评论:
我不能使用CountDownLatch,因为我事先不知道我将从资源中收集多少个唯一链接。
首先,vsminkov就是为什么awaitTermniation
会等待10分钟的答案。我会提供另一种解决方案。
而不是使用CountDownLatch
使用Phaser。对于每个新任务,您都可以注册并等待完成。
每次调用register
时创建一个移植器execute.submit
,每次arrive
完成时创建Runnable
。
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
}
答案 2 :(得分:0)
你没有打电话给关机。
这可能有效--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表示,这是最后一个,没有其他正在运行,没有在队列中挂起。没有任务会提交任何新的子任务。
答案 3 :(得分: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问题:
wait until all threads finish their work in java
我更喜欢使用最适合您用例的invokeAll()
或ForkJoinPool()
。