使用ExecutorService的优点是什么?

57
使用ExecutorService相比将Runnable传递到Thread构造函数中运行线程有什么优势?
9个回答

53

ExecutorService 将与底层抽象(如原始 Thread)相关的许多复杂性进行了抽象。它提供了安全启动、关闭、提交、执行和阻塞任务的机制,任务可以表示为 RunnableCallable

出自 JCiP 第6.2节,直接引用:

Executor 可能是一个简单的接口,但它构成了一个灵活和强大的异步任务执行框架的基础,支持广泛的任务执行策略。它提供了一种标准的方法来解耦 任务提交任务执行,将任务描述为 RunnableExecutor 实现还提供了生命周期支持和钩子,用于添加统计信息收集、应用程序管理和监视。 ... 在应用程序中使用 Executor 通常是实现生产者-消费者设计的最简单方法。

与其花费时间实现并行基础设施(通常难以正确完成且需要很大的努力),不如使用 j.u.concurrent 框架来专注于结构化任务、依赖关系和潜在并行性。对于大部分并发应用程序,很容易识别和利用任务边界并使用 j.u.c,这使您可以专注于可能需要更专门的解决方案的更小子集的真正并发挑战。

此外,尽管看起来有些样板代码的感觉,Oracle API 概述并发实用程序页面 包括一些真正坚实的使用它们的理由,其中最重要的是:

开发人员可能已经了解标准库类,因此无需学习临时并行组件的 API 和行为。此外,构建在可靠、经过充分测试的组件上的并发应用程序要简单得多。

Java并发实践是一本关于并发的好书。如果你还没有阅读过,建议你去获取一份。这本书全面地介绍了并发编程,远超出了这个问题的范畴,而且能够在很长时间内帮助你避免许多不必要的麻烦。


1
在Java中的Thread类上下文中,“解耦任务提交和任务执行”是什么意思? - legend

20

我认为使用ExecutorService的一个优点在于管理/调度多个线程。使用ExecutorService,您不必编写自己的线程管理器,这可以避免出现错误。如果程序需要同时运行多个线程,则特别有用。例如,您想一次执行两个线程,可以像这样轻松完成:

ExecutorService exec = Executors.newFixedThreadPool(2);

exec.execute(new Runnable() {
  public void run() {
    System.out.println("Hello world");
  }
});

exec.shutdown();

这个例子可能很简单,但是试着想象一下,“hello world”行包含了一个复杂的操作,并且你想让该操作在多个线程中同时运行,以提高程序的性能。这只是一个例子,还有许多情况需要安排或运行多个线程并使用ExecutorService作为您的线程管理器。

对于单个线程的运行,我没有看到使用ExecutorService的明显优势。


难道不是exec.execute(new Runnable()..吗? - softwarematter
无论哪种方式都可以,因为Thread实现了Runnable接口。对于简单情况,使用Runnable应该足够了。 - Manny
1
我真的不认为仅需要一个Runnable时创建一个Thread有任何意义......你甚至没有启动该Thread,所以这只会增加混乱和不必要的负担。 - ColinD
AYT,已应用审核意见。 :) - Manny
1
在finally语句中进行null检查并执行exec.shutdown();是一个很好的实践。 - Aniket Thakur

15

Executor框架(内置线程池框架)克服了传统Thread的以下限制:

  • 资源管理不当,即它为每个请求不断创建新资源。没有限制创建资源的数量。使用Executor框架,我们可以重用现有资源并限制创建资源的数量。
  • 不够健壮:如果我们不断创建新线程,我们将得到StackOverflowException异常,因此JVM会崩溃。
  • 创建时间开销大:对于每个请求,我们需要创建新资源。创建新资源是耗时的。即线程创建 > 任务。使用Executor框架,我们可以获得内置线程池。

线程池的好处

  • 使用线程池通过避免在请求或任务处理期间创建线程来减少响应时间。

  • 使用线程池允许您根据需要更改执行策略。您只需替换ExecutorService实现,就可以从单个线程转换为多个线程。

  • Java应用程序中的线程池通过创建一个基于系统负载和可用资源决定的配置线程数来增加系统的稳定性。

  • 线程池使应用程序开发人员摆脱了线程管理的烦恼,并允许专注于业务逻辑。

来源


8
以下是一些好处:
  1. 执行器服务以异步方式管理线程
  2. 使用Future callable在线程完成后获取返回结果。
  3. 管理将工作分配给空闲线程并重新分配已完成的工作以自动分配新工作
  4. fork-join框架用于并行处理
  5. 线程间通信更加顺畅
  6. invokeAll和invokeAny可以更好地控制一次性运行所有或某些线程
  7. 关闭功能可以完成所有线程分配的工作
  8. 定时执行器服务提供了生成可重复调用的Runnable和Callable方法,希望对您有所帮助

1
第二点不应该是“Callable”,而是“Future”吗?Future 是我们可以在线程完成后检索结果/值的地方。 - AmitG
1
是的,例如 Future<String> future = executorService.submit(callable); - Vinit Mehta

4

创建一个新线程真的那么昂贵吗?

作为一个基准测试,我用一个空的run()方法创建了60,000个带有Runnable的线程。在创建每个线程后,我立即调用了其start()方法。这需要大约30秒的强烈CPU活动。针对这个问题已经进行了类似的实验。总结是,如果线程不会立即完成,并且积累了大量的活动线程(几千个),那么就会出现问题:(1)每个线程都有一个堆栈,所以你会耗尽内存;(2)操作系统可能会对每个进程的线程数量设置限制,但似乎并不一定

因此,据我所知,如果我们说每秒启动10个线程,并且它们都比新线程更快地完成,而且我们可以保证不会超过太多这个速率,那么ExecutorService在可见性能或稳定性方面不提供任何具体优势。(虽然它仍然可以使表达某些并发想法的代码更方便或易读。)另一方面,如果您可能每秒调度数百或数千个需要一段时间才能运行的任务,则可能会立即遇到大问题。这可能会出现意外情况,例如在响应服务器请求时创建线程,并且您的服务器接收到请求强度激增。但是,例如对于每个用户输入事件(按键、鼠标移动)都有一个线程似乎完全没有问题,只要任务简短。


3

ExecutorService还提供了对FutureTask的访问,一旦后台任务完成,它将返回给调用类。在实现Callable时,这非常有用。

public class TaskOne implements Callable<String> {

@Override
public String call() throws Exception {
    String message = "Task One here. . .";
    return message;
    }
}

public class TaskTwo implements Callable<String> {

@Override
public String call() throws Exception {
    String message = "Task Two here . . . ";
    return message;
    }
}

// from the calling class

ExecutorService service = Executors.newFixedThreadPool(2);
    // set of Callable types
    Set<Callable<String>>callables = new HashSet<Callable<String>>();
    // add tasks to Set
    callables.add(new TaskOne());
    callables.add(new TaskTwo());
    // list of Future<String> types stores the result of invokeAll()
    List<Future<String>>futures = service.invokeAll(callables);
    // iterate through the list and print results from get();
    for(Future<String>future : futures) {
        System.out.println(future.get());
    }

2

在Java 1.5版本之前,Thread/Runnable被设计用于两个独立的服务:

  1. 工作单元
  2. 执行该工作单元

ExecutorService通过指定Runnable/Callable作为工作单元和Executor作为执行(带有生命周期)工作单元的机制来解耦这两个服务。


1

执行器框架

//Task
Runnable someTask = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World!");
    }
};

//Thread
Thread thread = new Thread(someTask);
thread.start();

//Executor 
Executor executor = new Executor() {
    @Override
    public void execute(Runnable command) {
        Thread thread = new Thread(someTask);
        thread.start();
    }
};

Executor 是一个接口,用于接受 Runnableexecute() 方法可以调用 command.run() 或者与其他使用 Runnable 的类(例如 Thread)一起工作。

interface Executor
    execute(Runnable command)

ExecutorService 接口扩展了 Executor 并添加了管理方法 - shutdown()submit(),它返回 Future[关于] - get()cancel()

interface ExecutorService extends Executor 
    Future<?> submit(Runnable task)
    shutdown()
    ...

ScheduledExecutorServiceExecutorService 的扩展,用于计划执行任务。

interface ScheduledExecutorService extends ExecutorService
    schedule()

Executors 类是一个工厂,提供 ExecutorService 实现来运行 async 任务[关于]

class Executors 
    newFixedThreadPool() returns ThreadPoolExecutor
    newCachedThreadPool() returns ThreadPoolExecutor
    newSingleThreadExecutor() returns FinalizableDelegatedExecutorService
    newWorkStealingPool() returns ForkJoinPool
    newSingleThreadScheduledExecutor() returns DelegatedScheduledExecutorService
    newScheduledThreadPool() returns ScheduledThreadPoolExecutor
    ...

结论

使用Thread对CPU和内存来说是一项昂贵的操作。ThreadPoolExecutor由任务队列(BlockingQueue)和线程池(一组Worker)组成,具有更好的性能和处理异步任务的API。


0

创建大量线程且没有对最大阈值进行限制可能会导致应用程序耗尽堆内存。因此,创建线程池是更好的解决方案。使用线程池,我们可以限制可池化和重复使用的线程数量。

Executors框架简化了在Java中创建线程池的过程。Executors类提供了ThreadPoolExecutor的ExecutorService的简单实现。

来源:

什么是 Executors Framework


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