为什么使用start()调用的10000个线程比使用run()调用的10000个线程需要更多时间?

6
我正在进行一个线程的“hello world”示例,我创建了一个简单的线程,使用run()调用(它只是一个普通的方法调用),并使用start()调用创建了一个重复的线程,该调用生成另一个线程进行处理。然而,start()调用所需的时间比run()调用所需的时间更长,后者不是线程调用,为什么会这样呢? < p>启动调用所需时间:00:00:08:300< /p>
    long time = System.currentTimeMillis();

    for (int i = 0; i < 100000; i++) {
        Thread thread = new Thread(new Car());
        thread.setName(Integer.toString(i));
        thread.start();
    }

    long completedIn = System.currentTimeMillis() - time;

    System.out.println(DurationFormatUtils.formatDuration(completedIn,
            "HH:mm:ss:SS"));

运行调用所需时间:00:00:01:366

    long time = System.currentTimeMillis();

    for (int i = 0; i < 100000; i++) {
        Thread thread = new Thread(new Car());
        thread.setName(Integer.toString(i));
        thread.run();
    }

    long completedIn = System.currentTimeMillis() - time;

    System.out.println(DurationFormatUtils.formatDuration(completedIn,
            "HH:mm:ss:SS"));
6个回答

7
从评论到被接受的回复:“你有什么建议吗?”
是的,不要直接使用线程。自Java 5以来,我们拥有了java.util.concurrent框架,它允许简化线程和任务管理。
创建线程是昂贵的。
即使在线程运行所需的任务之前,它也必须被创建,这是一个非常漫长的过程。正因为如此,我们有了线程池的概念。
不要每次想要执行并发任务时都创建新线程,而是首先创建线程,然后在需要时发送任务给它们。
线程池有第二个优点。当任务完成时,线程不会被销毁,而是保持活动状态以运行下一个任务,因此线程创建成本仅在初始化时发生一次。
那么如何使用这些概念呢?
首先创建一个带有线程池的执行器。为了保持简单,我们创建了一个具有100个线程的线程池(因为您想模拟100个并发调用的负载)。
ExecutorService pool = Executors.newFixedThreadPool(100);

然后你将提交你的任务:
long time = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
  pool.execute(new Car());
}

重要的是,在停止程序之前,您需要等待所有任务完成!!!

pool.shutdown(); //Do no longer accept new tasks.
pool.awaitTermination(1, TimeUnit.HOURS); //Wait for up to one hour for all tasks to finish.
long completedIn = System.currentTimeMillis() - time;
System.out.println(DurationFormatUtils.formatDuration(completedIn,
            "HH:mm:ss:SS"));

在您的线程代码中,您没有等待线程完成任务,实际上您测量了线程创建时间而非任务运行时间。 该代码做什么? executor.execute方法在一个线程中执行提供的任务。这里它把100个线程中的一个,并让它执行任务。 如果超过100个任务会发生什么? 使用100个线程,您不能同时运行超过100个任务。其他任务将排队等待,直到一个任务完成,因此一个线程可用于执行它。这是一种不会创建太多线程并且不会出现OutOfMemory或其他不良情况的好方法。 应该使用多少个线程? 这取决于您要执行的任务类型。
如果像Web服务器一样,您大部分时间都在等待IO(例如等待数据库获取数据,然后等待网络发送响应),那么您的线程大部分时间都会处于等待状态。因此,即使是一个CPU也能从数十甚至数百个线程中受益。即使更多线程开始减慢整个应用程序的速度,它也可以处理用户请求而不是让其等待或拒绝响应。
如果您的任务是CPU密集型的,则需要使用与CPU核心数量相同的任务,以最大限度地利用硬件但限制上下文切换的开销。对于具有4个核心超线程CPU,可以达到8个并发线程。
这个答案只是简短介绍...您可以通过查看java.util.concurrent包和阅读一些教程来学习更多。

6
start 实际上创建了一个新的线程(重操作),而 run 则在当前线程上调用线程对象的 run 方法(简单方法调用 - 轻操作)。
关于 start 的文档如下:
引起该线程开始执行;Java虚拟机调用这个线程的run方法。结果是两个线程同时运行:当前线程(从调用start方法返回)和另一个线程(执行其run方法)。

3
您不应该直接调用run方法。您所做的是创建了一堆Thread对象,但您实际上没有创建新的线程;因为您直接调用run方法,所以只在主线程上运行代码。

在大多数当前计算机上(我排除高端的千万级机器),创建100,000个线程的性能表现不佳。一旦您拥有的线程数超过CPU支持的容量,就会开始引起上下文切换。因此,如果您有一个四核系统,则运行超过四个线程将实际上会减慢程序的速度(模除I/O操作等,在这种情况下,CPU将处于空闲状态)。


我正在学习线程,因为我想能够模拟对目标方法的负载,似乎不能通过创建“100000”个线程来简单实现。你有什么建议吗? - OCB
1
@Chin:显然你不知道自己在做什么,但是你可以使用Runtime.getRuntime().availableProcessors()来查找可用的核心数,并为每个核心启动一个线程,运行一个紧密循环,反复调用该方法。 - T.J. Crowder
@Chin:你说的“模拟对目标方法调用的负载”是什么意思?这是一种性能分析吗?线程又有什么作用呢? - Lenik
@Leniik: 我的意思是调用我的 EJB 类中的一个方法来模拟负载,即每次100个并发调用。@请参见http://stackoverflow.com/questions/6805961/unable-to-achieve-load-testing-on-ejb-subject-using-threading - OCB

1

run() 方法是线程启动的方法,但它只是 Runnable 接口的一个简单方法。

Thread.start() 方法将创建一个新的线程(执行线程,真正的线程),该线程将执行您的线程实例的 run() 方法。

可以在线程实例上调用 run() 方法,因为有两种方式可以定义执行线程将执行的内容。

  • 旧方法:扩展 Thread 的 run() 方法。出于某些原因,包括耦合,不建议再使用此方法。

  • 新方法:将 Runnable(提供 run() 方法)传递给线程的构造函数。

请注意,如果您使用 Runnable(目标)创建线程并覆盖线程的 run() 方法,则可以理解您当前提供了两个要执行的代码,但哪个将被执行?如果查看 Thread 的默认 run() 实现,就很容易找到答案:

public void run() {
    if (target != null) {
        target.run();
    }
}

如果您覆盖了该方法,则除非您在覆盖上调用它,否则提供的Runnable目标将被绕过。

正如我所说,启动线程有两种方式。我猜当他们设计“新方式”时,由于向后兼容性问题,他们不得不保留Thread类中的run()方法,但如果他们可以回到过去,也许他们不会使Thread类实现Runnable接口,以便减少关于如何启动线程的混淆...


1

当您启动一堆线程并且只有一个或两个核心时,上下文切换所花费的时间加上运行任务的时间很容易超过按顺序一个接一个地运行代码所需的时间。


1

start方法是魔法所在。它会生成一个新线程,这需要时间:

  • 创建线程
  • 启动线程
  • 管理线程生命周期

最好的解释将在java.lang.Thread的源代码中找到。


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