ExecutorService
接口及相关API细节详解。
。
这些问题的设计宗旨,主要是测试面试者对Java语言的了解程度,而不是为了用弯弯绕绕的手段把面试者搞蒙。
如果你看过往期的问题,就会发现每一个都不简单。
这些试题模拟了认证考试中的一些难题。 而 “中级(intermediate)” 和 “高级(advanced)” 指的是试题难度,而不是说这些知识本身很深。 一般来说,“高级”问题会稍微难一点。
问题(高级难度)
此问题的目的是考察如何通过 Runnable
和 Callable
来创建任务,并使用 ExecutorService
来并发执行。
我们有一个 Logger
类,定义如下所示:
class Logger implements Runnable {
String msg;
public Logger(String msg) {
this.msg = msg;
}
public void run() {
System.out.print(msg);
}
}
并给出如下使用的代码:
Stream<Logger> s = Stream.of(
new Logger("Error "),
new Logger("Warning "),
new Logger("Debug "));
ExecutorService es =
Executors.newCachedThreadPool();
s.sequential().forEach(l -> es.execute(l));
es.shutdown();
es.awaitTermination(10, TimeUnit.SECONDS);
这里省略了相关的 import
语句, 假设代码能编译并正常启动。 请选择两项可能的输出结果:
- A、 Error Debug Warning
- B、 Error Warning Debug
- C、 Error Error Debug
- D、 Error Debug
答案和解析
这道试题属于 Executors
类和 ExecutorService
接口相关的考点,顺带考察 Executors
工具类自带的 ExecutorService
线程池实现。
在Java的早期版本中,需要程序员手工创建和管理线程。线程是系统内核级的重要资源,并不能无限创建; 而且创建线程的开销很大,所以开发中一般会使用资源池模式,也就是创建 “线程池”。通过线程池,可以用少量的线程,来执行大量的任务。
线程池的思路是这样的: 与其为每个任务创建一个线程,执行完就销毁; 倒不如统一创建少量的线程, 然后将任务逻辑用 Runnable
包装起来, 提交给线程池来调度执行。
有任务需要调度的时候,线程池找一个空闲的线程,并通知他干活。 任务执行完成后,再将这个线程放回池子里,等待下一次调度。
Java 5.0 开始提供标准的线程池API。 通过 Executor
和 ExecutorService
接口定义了线程池以及支持的交互操作。
另外,我们可以使用 Executors
的静态工厂方法来实例化 ExecutorService
的各种实现。
相关的基础类和接口都位于 java.util.concurrent
包中, 在编写简单的并发任务时,可以直接使用。
Executor
是顶层接口, 定义了执行 Runnable
任务的方法;但我们一般用的是子接口 ExecutorService
及其实现。
ExecutorService
接口中增加了处理 Callable
的方法, 以及关闭线程池的功能。
实现 Callable
接口的任务会返回一个结果, 调用方可以通过提交任务时返回的 Future
对象,来异步获取任务的执行状态和结果,这样就对任务有了一定的管理和控制能力。
Executor
和 ExecutorService
接口并没有规定使用哪种调度策略来执行。
- 有些线程池,使用固定数量的线程来并发地执行任务,新提交的任务要等到有空闲线程才会被执行。
- 有的线程池, 在工作负载上升时自动增加线程,并在需求降低时清理掉一部分线程。
- 还有的线程池只使用单个线程,直接按顺序执行提交的任务。
这些特征取决于具体的实现,需要开发者根据业务系统的特征来权衡,并选择适当的线程池。 针对这几类线程池,Executors
工具类提供了三种工厂方法:
newFixedThreadPool
newCachedThreadPool
newSingleThreadExecutor
前两个方法创建的线程池可以有多个worker线程, 而 newSingleThreadExecutor
方法创建的线程池则只有单个线程。
回到前面的问题, 试题中给出的代码创建了缓存模式的线程池。
这类线程池会根据需要生成新的worker线程,并清理一段时间内没有使用到的线程。
但缓存模式的线程池有一个严重缺点: 创建的线程数有可能不被限制, 那样的话会导致大量的资源占用。 在高负载场景下,可能会由于资源争用而导致性能急剧下降。
因为创建的线程池具有多个线程, 所以后面提交的任务可以并发执行。
无论谁先开始,我们都无法对其执行进度做出精确预测。
也就是说,他们输出消息的顺序可能是任意的。
由此得知, 选项A
和 选项B
都 正确
。
ExecutorService
会保证提交的任务最多被执行一次。
在某些情况下,任务可能不会执行,或者在执行完成之前线程池就被关闭了。
因为具有最多执行一次的特征,所以我们不会看到任何重复的消息。因此可以判断,选项C不正确
。
在调用 shutdown
方法之后,ExecutorService
会拒绝新的任务提交请求, 但已有的任务会继续运行,直到所有的作业全部执行完才会关闭。
因此在这里给的代码中, 三个消息都会看到。 因此可知,选项D不正确
。
顺便提一句,可能有些读者会认为,如果在10秒内执行不完, 那么选项D也可能是正确的。
但反过来说,如何确定这个消息会被打印呢?
因为试题中给出的任务逻辑非常简单,很明显不可能10秒钟还执行不完。
而且我们通过分析能判断出 选项A
和 选项B
是正确的, 那么做题时就可以将这种不可能的情况排除。
当然,你可能对选项D感兴趣,因为在其他某些极端的情况下, 作业无法在10秒内完成,比如恰好在这个时刻操作系统启动升级或更新。
请注意,在给定的代码中,没有任何证据表明 JVM 将被强行关闭。
而且默认创建的线程都是 非守护线程
(nondaemon thread),因此,在作业完成之前,JVM 不会退出。
所以,如果允许程序运行,则对应的消息都会被打印出来。
总结
正确的选项是 A
和 B
。