ExecutorService与普通线程生成器的区别

29

我有一个关于Java中ExecutorService的基本问题。

很难看出简单地创建Threads以并行执行某些任务和将每个任务分配给ThreadPool之间的区别。

ExecutorService看起来也很简单高效,所以我想知道为什么我们不总是使用它。

这只是一种方式比另一种方式更快地执行其工作的问题吗?

这里有两个非常简单的示例,以展示两种方式之间的差异:

使用执行器服务:Hello World(任务)

static class HelloTask implements Runnable {
    String msg;

    public HelloTask(String msg) {
        this.msg = msg; 
    }
    public void run() {
        long id = Thread.currentThread().getId();
        System.out.println(msg + " from thread:" + id);
    }
}

使用执行器服务: Hello World(创建执行器,提交任务)

static class HelloTask {
    public static void main(String[] args) {
        int ntasks = 1000;
        ExecutorService exs = Executors.newFixedThreadPool(4);

        for (int i=0; i<ntasks; i++) { 
            HelloTask t = new HelloTask("Hello from task " + i);    
            exs.submit(t);
        }
        exs.shutdown();
    }
}

下面展示了一个类似的例子,但是扩展了Callable接口。您能告诉我两者之间的区别,以及在哪些情况下应该使用特定的方法而不是另一个吗?
使用Executor Service:计数器(任务)
static class HelloTaskRet implements Callable<Long> {
    String msg;

    public HelloTaskRet(String msg) {
        this.msg = msg; }

        public Long call() {
        long tid = Thread.currentThread().getId(); 
        System.out.println(msg + " from thread:" + tid); 
        return tid;
    } 
}

使用执行器服务:(创建,提交)

static class HelloTaskRet {
    public static void main(String[] args) {
        int ntasks = 1000;
        ExecutorService exs = Executors.newFixedThreadPool(4);

        Future<Long>[] futures = (Future<Long>[]) new Future[ntasks];

        for (int i=0; i<ntasks; i++) { 
            HelloTaskRet t = new HelloTaskRet("Hello from task " + i);
            futures[i] = exs.submit(t);
        }
        exs.shutdown();
    }
}

2
这两个示例都使用了ExecutorService而不是创建新的线程,因此我不确定您在这种情况下要比较什么。您是否困惑于何时使用Runnable和何时使用Callable - Dioxin
2个回答

40
虽然问题和示例代码没有关联,但我会尽力澄清两者。
相比于随意创建线程,使用ExecutorService的优势在于它的行为是可预测的,并且避免了线程创建的开销,这在JVM上相对较大(例如,它需要为每个线程保留内存)。
所谓可预测性,是指您可以控制并发线程的数量,并且您知道何时以及如何创建和销毁它们(这样您的JVM在突发高峰时不会崩溃,线程也不会被遗弃导致内存泄漏)。您可以将ExecutorService实例传递给程序的各个部分,以便它们可以向其提交任务,同时您仍然可以在单个位置进行完全透明的管理。然后,您可以根据配置或环境替换确切的实现。例如,您可能希望根据可用CPU的数量拥有不同数量的线程池线程。
ExecutorService还可以精确地限定范围,并在范围退出时关闭(通过shutdown()方法)。
fixedThreadPool使用一个线程池,其大小不会超过分配的大小。
一个`cachedThreadPool`没有最大限制,但会在一段时间内重用缓存的线程。它主要用于需要频繁并发执行许多小任务的情况。
一个`singleThreadExecutor`用于串行执行异步任务。
还有其他类型的线程池,比如`newScheduledThreadPool`用于定期重复任务,`newWorkStealingPool`用于可以分解为子任务的任务,还有其他一些。请查看`Executors`类以了解详细信息。
在JVM 18中,虚拟线程是一种新的概念,可以以较低的成本创建。通过`newVirtualThreadPerTaskExecutor`可以获得每次创建新虚拟线程的`ExecutorService`。与手动创建线程相比,使用这种方式的优势不仅仅是性能,更重要的是结构上的优势,它允许作用域和其他上述好处,并与现有的期望`ExecutorService`的API进行互操作。
现在,关于RunnableCallable的话题,从你的例子中很容易看出来。Callable可以返回一个值占位符(Future),将来最终会被实际值填充。而Runnable则不能返回任何东西。此外,Runnable也不能抛出异常,而Callable可以。

20

ExecutorService 相比于普通线程提供了许多优势:

  1. 您可以创建/管理/控制 线程 的生命周期,并优化线程创建成本开销。
  2. 您可以 控制任务的处理方式(如工作窃取、ForkJoinPool、invokeAll 等)。
  3. 您可以在 将来某个时间 安排任务。
  4. 您可以 监控线程的进展和健康状况

即使是单线程,我也更喜欢使用 Executors.newFixedThreadPool(1);

请查看相关的 SE 问题:

Java 的 Fork/Join 和 ExecutorService - 何时使用哪个?

使用 ExecutorService 有什么优点?


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