点击上方“Java后端技术栈“关注
持续推送技术干货
ExecutorServic
e 接口及相关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 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。
原文:https://renfufei.blog.csdn.net/article/details/104726229
往期精选
Spring MVC 面试题和答案
面试被问频率最高的几道Redis面试题
ThreadLocal 面试六连问,中高级必问
Spring Boot系列--面试题和参考答案
面试必问:Spring循环依赖的三种方式
10 大 Java面试难题,打趴无数面试者!
【文章汇总】面试篇
在这金三银四的季节,栈长为大家准备了四份面试宝典:
《java面试宝典5.0》
《Java(BAT)面试必备》
《350道Java面试题:整理自100+公司》
《资深java面试宝典-视频版》
大量电子书籍
分别适用于初中级,中高级,以及资深级工程师的面试复习。
内容包含java基础、javaweb、各个性能优化、JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构、限流熔断降级等等。
获取方式:点“在看”,V信扫描上面二维码:注明面试领取,更多精彩陆续奉上。