ExecutorService 与休闲线程 Spawner

问题描述 投票:0回答:2

我有一个关于

ExecutorService
在 Java 中如何工作的基本问题。

很难看出简单创建

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 接口,您能告诉我两者之间的区别吗?在什么情况下应该使用特定的一个而不是另一个?

使用执行器服务:计数器(任务)

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();
    }
}
java multithreading executorservice
2个回答
40
投票

虽然问题和示例代码不相关,但我会尝试澄清两者。

ExecutorService
相对于随意生成线程的优势在于,它的行为可预测并且避免了线程创建的开销,这在 JVM 上相对较大(例如,它需要为每个线程保留内存)。

通过可预测性,我的意思是你可以控制并发线程的数量,并且你知道它们何时以及如何被创建和销毁(这样你的 JVM 就不会在突然达到峰值的情况下崩溃,并且线程也不会被遗弃)内存泄漏)。您可以传递一个

ExecutorService
实例,以便程序的各个部分可以向其提交任务,同时您仍然可以在一个位置完全透明地管理它。然后您可以替换基于的确切实现,例如关于配置或环境。例如,您可能希望根据可用 CPU 的数量拥有不同数量的池线程。
ExecutorService
也可以精确地设置范围,并在退出范围时关闭(通过
shutdown()
)。

A

fixedThreadPool
使用一个线程池,该线程池的增长不会超出其分配的范围。

A

cachedThreadPool
没有最大值,但会在一段时间内重用缓存的线程。它主要用于需要在单独的线程上执行许多小任务的情况。

A

singleThreadExecutor
用于串行执行的异步任务。

还有其他任务,例如

newScheduledThreadPool
用于定期重复任务,
newWorkStealingPool
用于可以分为子任务和其他一些任务。探索
Executors
课程以了解详细信息。

从 JVM 18 开始,虚拟线程已经成为现实,而且创建虚拟线程的成本很低,因此它们显着改变了情况。每次创建一个新虚拟线程的

ExecutorService
可以通过
newVirtualThreadPerTaskExecutor
获得。与手动生成线程相比,使用此方法的优点并不像structural那样以性能为中心,因为它允许范围界定和上面解释的其他好处,以及与期望
ExecutorService
的现有API的互操作性。

现在,关于

Runnable
Callable
的主题,从您的示例中很容易看出。
Callable
s 可以返回一个值占位符 (
Future
),该占位符最终将由未来的实际值填充。
Runnable
s 无法返回任何内容。此外,
Runnable
也不能抛出异常,而
Callable
可以。


20
投票

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

  1. 您可以创建/管理/控制线程的生命周期并优化线程创建成本开销
  2. 您可以控制任务的处理(Work Stealing、ForkJoinPool、invokeAll)等。
  3. 您可以在未来时间
  4. 安排任务
  5. 您可以监控线程的进度和运行状况

即使对于单个线程,我也更喜欢使用

Executors.newFixedThreadPool(1);

看看相关的SE问题:

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

使用 ExecutorService 有哪些优势?

© www.soinside.com 2019 - 2024. All rights reserved.