我有一个关于
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();
}
}
虽然问题和示例代码不相关,但我会尝试澄清两者。
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
可以。
ExecutorService与普通线程相比提供了许多优势
即使对于单个线程,我也更喜欢使用
Executors.newFixedThreadPool(1);
看看相关的SE问题: