如何使循环更快——多线程是一个选择吗?最佳实践。

8

我使用URL中的索引调用一个Api服务;例如,最后一个索引是:420.555。我的做法如下:

for(int i =0; i <= 420555;i++){
  url = new URl("https://someURL/"+ i);
  read the json with BufferedReader reader = new BufferedReader( new InputStreamReader( url.openStream(), "UTF-8" ) )) {

   create object from json
   save the result to my DB
}

性能非常差。(当然,我的数据库中有很多条目,但需要超过6小时并且因为JAVA VM中的内存已满而崩溃。)您有任何想法如何更快地完成此操作吗?如果需要完整的代码,我可以发布它。但我认为for循环是问题所在...我的想法是使用多线程,但我以前从未使用过,并且不确定是否最佳实践。当多线程是最佳实践时,您能否给我一个此案例的示例?

5
多线程不能解决你的内存问题。 - assylias
对于内存问题,我可以尝试在每次循环结束时关闭 BufferedReader。至于时间...你正在进行超过400k个WebSocket请求。你的下载速度是多少? - TurnipEntropy
我没有首先寻找解决我的内存问题的答案。我想先让它更快。我可以增加内存...我的下载速度约为200k。 - Damlo
你需要按特定顺序获取数据吗? - Mickael
6个回答

5
您的代码所做的事情是:
  • 从URL获取内容并将其作为 JSON 解析
  • 做一些其他处理,然后将结果保存到数据库中

这些步骤是按顺序执行的。

因此,当然,在并行处理这些循环体时可以缩短总体执行时间。但这对于内存问题并没有帮助。正如评论所指出的那样,该问题更可能是由您的代码中的错误引起的(例如未正确关闭资源)。

当然,这会引入新的问题;例如处理用于数据库访问的连接池。

为了添加“多个线程”,直接的方法是将任务提交到 ExecutorService 中 - 有关示例,请参见 此处

最后:第一个真正的答案是退后一步。似乎你已经很难把手头的任务做好了!增加复杂性可能有助于解决某些问题;但绝对必须确保您的代码在“顺序模式”下完全正确和工作之后,再添加“多个线程”的功能。否则,您将快速遇到其他问题,并且以不太确定但更难以调试的方式运行。

第二个真正的答案是:发出 40 万个请求 永远不是 一个好主意。无论是按顺序还是并行执行都不行。实际的解决方案是退后一步,更改该 API 并允许例如 批量读取。不要在 400K 个请求中下载 400K 个对象。例如每次进行 100 次请求并下载 4K 个对象。

长话短说:您的 真正 的问题是正在使用的 API 的设计。除非您更改它,否则您将不会解决问题而只是与症状作斗争。


我无法更改API。那不是我的API。我只需要从那里获取数据。 - Damlo
正如所说:那么你能做的并不是太多;除了A)修复你当前的代码;当它可以工作时,B)使用执行器服务并行进行请求。但正如所说:这些40万个网络请求会消耗大量资源。 - GhostCat
我已经将我的代码添加到了Github Gist上: https://gist.github.com/EikeBierman/fde05dde2e09d6167f76c41fe41828f1 - Damlo

2

同时进行 For 循环可以加快处理速度。这里提供一个多线程解决方案的示例:

//set THREADS_COUNT for controlling concurrency level
int THREADS_COUNT=8;

//make a shared repository for your URLs
ConcurrentLinkedQueue<Integer> indexRepository=new ConcurrentLinkedQueue<Integer>();
for(int i=0;i< 420555;i++)
    indexRepository.add(i);

// Define a ExecutorService which providing us multiple threads
ExecutorService executor = Executors.newFixedThreadPool(THREADS_COUNT);

//create multiple tasks (the count is the same as our threads)
for (int i = 0; i < THREADS_COUNT; i++)
    executor.execute(new Runnable() {
        public void run() {
            while(!indexRepository.isEmpty()){
                url = new URl("https://someURL/"+ indexRepository.remove());
                //read the json with BufferedReader reader = new BufferedReader( new InputStreamReader( url.openStream(), "UTF-8" ) )) {
                //create object from json
                //save the result to my DB
            }
        }
    });

executor.shutdown();

// Wait until all threads are finish
while (!executor.isTerminated()) {

}
System.out.println("\nFinished all threads");

请注意,如何处理数据库也会显著影响性能。使用批量插入或适当的事务可以提高性能。

1
您的问题有些混淆,但是看起来您的代码首先应该在每次交互后关闭流:
String url = "https://someURL/%d";
        for(int i =0; i <= 420555;i++){
            try (InputStreamReader fis = new InputStreamReader(new URL(String.format("https://someURL/",i)).openStream(), "UTF-8");
                    BufferedReader reader = new BufferedReader(fis)) {
                // do the job
            }
        }

这并不回答问题。 - Mickael
如果你仔细阅读问题,他问的是“最佳实践...”,那么最佳实践是打开流和缓冲读取器并保持它们打开吗?我认为不是,而我的帖子则回答了这个问题或其中的一部分。当然,还有许多其他技术可以做到这一点,例如批量调用其余部分等,这些技术可能会增加性能。 - fdam
此外,JVM 崩溃可能是由于已打开的流或省略的代码引起的。 - fdam
据我所知,使用 try (BufferedReader reader = new BufferedReader(new InputStreamReader(allmoviesUrl.openStream(),"UTF-8"))) 可以关闭 reader 对象,reader 又会关闭 InputStreamReaderInputStreamReader 最后会关闭 InputStream。除此之外,我同意并行化破损的代码只会让它更快地崩溃。我们称之为“失败快速”。 :D - maaartinus

1

是的。您可以使用Executors更快地完成它。

如果您正在使用java-8,请使用以下API。

public static ExecutorService newWorkStealingPool() 

创建一个工作窃取线程池,使用所有可用的处理器作为目标并行级别。

如果您没有使用Java 8,请使用

public static ExecutorService newFixedThreadPool(int nThreads)

将线程数设置为可用处理器数量

nThreads = Runtime.getRuntime().availableProcessors()

0
如果无法从外部API获取所需的数据,则只能并行处理以提高性能。
您可以将范围分成更小的块(例如[1-20],[21-40],...),然后创建具有一些池大小的ExecutorService,并同时处理每个块。
这将提高程序性能,但不是太多。还取决于您计算机的CPU。
GhostCat的解决方案是正确的,但我提出了另一种替代方案。如果无法获取超过400K请求的更多数据,则只有一种方法可以提高获取数据的性能。

0

我看到的另一个瓶颈是保存数据库。如果逐个保存,性能将会很低,因为它涉及I/O操作。更好的方法之一是分离读取器和写入器。

读取器:按块下载,例如批量大小为500。

写入器:使用批量大小为500保存到数据库。

如果进行分隔,则可以轻松扩展,根据需要增加读取线程和写入线程。或者一个线程将执行读/写一个块,即500。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接