【问题标题】:Understanding Threads + Asynchronous了解线程 + 异步
【发布时间】:2012-10-07 05:52:55
【问题描述】:

所以我制作了一个程序,需要向 URL 发送大量(例如 10,000+)GET 请求,并且我需要它尽可能快。当我第一次创建程序时,我只是将连接放入一个 for 循环中,但它真的很慢,因为它必须等待每个连接完成才能继续。我想让它更快,所以我尝试使用线程,它使它更快一些,但我仍然不满意。

我猜想解决这个问题的正确方法是使用异步连接并连接到所有 URL。这是正确的方法吗?

另外,我一直在尝试了解线程及其工作原理,但我似乎无法理解。我使用的计算机具有 Intel Core i7-3610QM 四核处理器。根据英特尔网站上关于这款处理器的规格,它有 8 个线程。这是否意味着我可以在 Java 应用程序中创建 8 个线程并且它们都将同时运行?超过8个就不会提速了?

任务管理器中“性能”选项卡下“线程”旁边的数字究竟代表什么?目前,我的任务管理器显示“线程”超过 1,000。为什么它是这个数字,如果我的处理器支持它,它怎么能超过 8? 我还注意到,当我尝试使用 500 个线程作为测试的程序时,任务管理器中的数字增加了 500,但它的速度与我将其设置为使用 8 个线程的速度相同。因此,如果数量随着我在 Java 应用程序中使用的线程数而增加,那么为什么速度相同?

另外,我尝试用 Java 中的线程做一个小测试,但输出对我来说没有意义。 这是我的测试课:

import java.text.SimpleDateFormat;
import java.util.Date;

public class Test {

    private static int numThreads = 3;
    private static int numLoops = 100000;
    private static SimpleDateFormat dateFormat = new SimpleDateFormat("[hh:mm:ss] ");

    public static void main(String[] args) throws Exception {

        for (int i=1; i<=numThreads; i++) {
            final int threadNum = i;
            new Thread(new Runnable() {
                public void run() {
                    System.out.println(dateFormat.format(new Date()) + "Start of thread: " + threadNum);
                    for (int i=0; i<numLoops; i++)
                        for (int j=0; j<numLoops; j++);
                    System.out.println(dateFormat.format(new Date()) + "End of thread: " + threadNum);
            }
            }).start();
            Thread.sleep(2000);
        }

    }
}

这会产生如下输出:

[09:48:51] Start of thread: 1
[09:48:53] Start of thread: 2
[09:48:55] Start of thread: 3
[09:48:55] End of thread: 3
[09:48:56] End of thread: 1
[09:48:58] End of thread: 2

为什么第三个线程立即开始和结束,而第一个和第二个线程各需要 5 秒?如果我添加了 3 个以上的线程,那么 2 以上的所有线程都会发生同样的事情。

对不起,如果这是一篇长篇文章,我有很多问题。 提前致谢。

【问题讨论】:

    标签: java multithreading asynchronous


    【解决方案1】:

    此解决方案更具体地解决了尝试尽可能快地发出 10k 个请求的一般问题。我建议您放弃 Java HTTP 库并改用 Apache 的 HttpClient。他们有几个suggestions 用于最大化性能,这可能很有用。我听说 Apache HttpClient 库通常也更快,重量更轻,开销更少。

    【讨论】:

      【解决方案2】:

      一些概念:

      您可以在系统中拥有许多线程,但在任何时间点,只有其中一些(在您的情况下最多 8 个)会在 CPU 上“调度”。因此,您无法获得比 8 个并行运行的线程更高的性能。事实上,随着线程数量的增加,性能可能会下降,因为创建、销毁和管理线程所涉及的工作。

      线程可以处于不同的状态:http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/Thread.State.html 在这些状态中,RUNNABLE 线程可以获得 CPU 时间片。操作系统决定将 CPU 时间分配给线程。在具有 1000 多个线程的常规系统中,某个线程何时获得 CPU 时间以及它将在 CPU 上占用多长时间是完全不可预测的。

      关于您要解决的问题:

      您似乎找到了正确的解决方案 - 发出并行异步网络请求。但是,实际上,同时启动 10000 多个线程和如此多的网络连接可能会对系统资源造成压力,并且可能无法正常工作。这个post 对使用Java 的异步I/O 有很多建议。 (提示:不要只看接受的答案)

      【讨论】:

        【解决方案3】:

        您的处理器有 8 个内核,而不是线程。这实际上意味着在任何给定时刻只能运行 8 件事情。但是,这并不意味着您只能使用 8 个线程。

        当线程同步打开到 URL 的连接时,它通常会在等待远程服务器返回时休眠。当该线程处于休眠状态时,其他线程可以工作。如果您有 500 个线程并且所有 500 个线程都在休眠,那么您没有使用 CPU 的任何内核。

        另一方面,如果您有 500 个线程并且所有 500 个线程都想做某事,那么它们就不能同时运行。为了处理这种情况,有一个特殊的工具。处理器(或者更可能是操作系统或两者的某种组合)有一个调度程序,该调度程序确定哪些线程在任何给定时间在处理器上主动运行。有许多不同的规则,有时是随机活动来控制这些调度程序的工作方式。这可以解释为什么在上面的示例中线程 3 似乎总是先完成。也许调度程序更喜欢线程 3,因为它是主线程最近调度的线程,有时无法预测其行为。

        现在回答您关于性能的问题。如果打开连接从不涉及睡眠,那么同步或异步处理事情都无关紧要,您将无法获得超过 8 个线程的任何性能提升。实际上,打开连接所涉及的很多时间都花在了睡觉上。异步和同步之间的区别在于如何处理睡眠所花费的时间。从理论上讲,您应该能够在两者之间获得几乎相同的性能。

        使用多线程模型,您只需创建比内核数量更多的线程。当线程进入睡眠状态时,它们会让其他线程工作。这有时会更容易处理,因为您不必编写线程之间的任何调度或交互。

        使用异步模型,您只需为每个内核创建一个线程。如果该线程需要休眠,那么它不会休眠,但实际上必须有代码来处理切换到下一个连接。例如,假设打开一个连接(A、B、C)有三个步骤:

        while (!connectionsList.isEmpty()) {
          for(Connection connection : connectionsList) {
        
            if connection.getState() == READY_FOR_A {
              connection.stepA();
              //this method should return immediately and the connection
              //should go into the waiting state for some time before going
              //into the READY_FOR_B state
            }
            if connection.getState() == READY_FOR_B {
              connection.stepB();
              //same immediate return behavior as above
            }
            if connection.getState() == READY_FOR_C {
              connection.stepC();
              //same immediate return behavior as above
            }
            if connection.getState() == WAITING {
              //Do nothing, skip over
            }
            if connection.getState() == FINISHED {
              connectionsList.remove(connection);  
            }
          }
        }
        

        请注意,线程在任何时候都不会休眠,因此线程数超过内核数是没有意义的。最终,是采用同步方法还是异步方法是个人喜好问题。只有在绝对极端情况下,两者之间才会存在性能差异,您需要花费很长时间进行分析才能达到应用程序的瓶颈。

        听起来您创建了很多线程并且没有获得任何性能提升。这可能有很多原因。

        • 您建立的连接可能实际上并未处于休眠状态,在这种情况下,我不希望看到超过 8 个线程的性能提升。我认为这不太可能。
        • 可能所有线程都在使用一些公共共享资源。在这种情况下,其他线程无法工作,因为休眠线程拥有共享资源。是否有所有线程共享的对象?这个对象有同步方法吗?
        • 您可能有自己的同步。这可能会产生上述问题。
        • 可能每个线程都必须执行某种设置/分配工作,这会破坏您通过使用多个线程获得的好处。

        如果我是你,我会在使用少量线程 (20) 运行时使用 JVisualVM 之类的工具来分析你的应用程序。 JVisualVM 有一个漂亮的彩色线程图,可以显示线程何时运行、阻塞或休眠。这将帮助您了解线程/核心关系,因为您应该看到正在运行的线程数少于您拥有的核心数。此外,如果您看到很多阻塞的线程,那么这可以帮助您找到瓶颈(如果您看到很多阻塞的线程使用 JVisualVM 在那个时间点创建线程转储并查看线程被阻塞的原因)。

        【讨论】:

        • 啊,我实际上无法编辑该评论... 5 分钟限制... “有没有所有线程共享的对象?这个对象有任何同步方法吗?”我所有的线程都在做同样的事情:它实例化一个 URL 对象并打开与代理的连接。它设置 URLConnection 连接和读取超时。然后它使用 BufferedReader 和 InputStreamReader 从 URLConnection 中读取。最后,它将一个单词写入文本文件。这就是每个线程正在做的事情,运行 500 个这些线程似乎并没有加快速度:/
        • 我做了一些挖掘工作。我怀疑 Java 有一个大小有限的底层连接池。有一个名为 http.maxConnections [参见此处] (docs.oracle.com/javase/1.4.2/docs/guide/net/properties.html) 的网络属性。默认值为 5。这意味着打开超过 5 个连接后,它们都使用相同的 5 个底层套接字(共享资源),之后打开的任何连接都将阻塞。同样,您可以使用 JVisualVM 来确认这一点。
        猜你喜欢
        • 2016-09-28
        • 1970-01-01
        • 2013-05-16
        • 2015-01-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-05-31
        • 1970-01-01
        相关资源
        最近更新 更多