我正在为网站编写解析器,它有很多页面(我称之为IndexPages)。每个页面都有很多链接(在IndexPage中大约有300到400个链接)。我使用Java的ExecutorService
在一个IndexPage中同时调用12 Callables
。每个Callable
只是向一个链接发出一个http请求,并进行一些解析和数据库存储操作。当第一个IndexPage完成时,程序进入第二个IndexPage,直到找不到下一个IndexPage。
在运行时,似乎没问题,我可以很好地观察线程的工作/调度。每个链接的解析/存储只需要大约1到2秒。
但随着时间的推移,我观察到每个Callable
(解析/存储)需要越来越长的时间。以这张照片为例,有时需要10秒或更长时间才能完成Callable
(绿色条是RUNNING,紫色条是WAITING)。而我的电脑正在陷入困境,一切都变得迟钝。
这是我的主要算法:
ExecutorService executorService = Executors.newFixedThreadPool(12);
String indexUrl = // Set initial (1st page) IndexPage
while(true)
{
String nextPage = // parse next page in the indexUrl
Set<Callable<Void>> callables = new HashSet<>();
for(String url : getUrls(indexUrl))
{
Callable callable = new ParserCallable(url , … and some DAOs);
callables.add(callable);
}
try {
executorService.invokeAll(callables);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (nextPage == null)
break;
indexUrl = nextPage;
} // true
executorService.shutdown();
该算法简单且不言自明。我想知道是什么原因导致这种情况?无论如何要防止这种性能下降?
CPU /内存/堆显示合理的使用情况。
环境,仅供参考。
====================更新====================
我已将我的实施从ExecutorService
更改为ForkJoinPool
:
ForkJoinPool pool=new ForkJoinPool(12);
String indexUrl = // Set initial (1st page) IndexPage
while(true)
{
Set<Callable<Void>> callables = new HashSet<>();
for(String url : for(String url : getUrls(indexUrl)))
{
Callable callable = new ParserCallable(url , DAOs...);
callables.add(callable);
}
pool.invokeAll(callables);
String nextPage = // parse next page in this indexUrl
if (nextPage == null)
break;
indexUrl = nextPage;
} // true
它需要比ExecutorService
解决方案更长的时间。 ExecutorService
大约需要2个小时才能完成所有页面,ForkJoinPool
需要3个小时,而每个Callable仍然需要更长时间才能完成(从1秒到5,6甚至10秒)。我不介意需要更长的时间,我只希望完成一份工作需要不间断的时间(不长也不长)。
我想知道我是否在解析器中创建了很多(非线程安全的)GregorianCalendar
,Date
和SimpleDateFormat
对象,并导致一些线程问题。但我没有重用这些对象或在线程之间传递它们。所以我仍然找不到原因。
答案 0 :(得分:1)
基于堆,您有内存问题。 ExecutorService.invokeAll
会将Callable
个实例的所有结果收集到List
中,并在List
完成后返回ExecutorService.submit
。您可能需要考虑简单地调用Callable
,因为您似乎并不关心每个{{1}}的结果。
答案 1 :(得分:0)
我无法理解为什么需要Callable来解析索引页面,因为你的'Caller'方法不期望ParserCallable产生任何结果。我可以看到您需要对异常处理进行排序,但仍然可以使用Runnable
进行管理。
当你使用Callable.call()
时,它会返回FutureTask,这是永远不会使用的。
您应该能够通过使用Runnable来改进实现,这可以避免这种额外的操作
ExecutorService executor = Executors.newFixedThreadPool(12);
for(String url : getUrls(indexUrl)) {
Runnable worker = new ParserRunnable(url , … and some DAOs);
executor.execute(worker);
}
class ParserRunnable implements Runnable{
}
答案 2 :(得分:0)
据我了解,如果你有40个页面,每个页面有~300个URL,你将创建~12,000个Callables?虽然可能没有太多的Callables,但它有很多HTTP连接和数据库连接。
我认为你应该尝试每页使用一个Callable。你通过并行运行它们仍然可以获得很多。我不知道你在为HTTP请求使用了什么,但是你可以在那里重用系统资源,而不是打开和关闭它们中的12,000个。
特别是对于DB。你只有40个连接。您甚至可以通过在本地收集~300条记录,然后使用批量更新来提高效率。