Java坑人面试题系列: 线程/线程池(高级难度)

点击上方“Java后端技术栈“关注

持续推送技术干货

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 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

往期精选

去 BAT 面试,总结了这 50 道 MySQL 面试题!

在家远程面试,该如何让面试官钟意你?

Spring MVC 面试题和答案

面试被问频率最高的几道Redis面试题

ThreadLocal 面试六连问,中高级必问

Spring Boot系列--面试题和参考答案

面试必问:Spring循环依赖的三种方式

10 大 Java面试难题,打趴无数面试者!

【文章汇总】面试篇

在这金三银四的季节,栈长为大家准备了四面试宝典:

  • 《java面试宝典5.0》

  • 《Java(BAT)面试必备》

  • 《350道Java面试题:整理自100+公司》

  • 《资深java面试宝典-视频版》

  • 大量电子书籍

分别适用于初中级,中高级,以及资深级工程师的面试复习。

内容包含java基础、javaweb、各个性能优化、JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构、限流熔断降级等等。

获取方式:点“在看”,V信扫描上面二维码:注明面试领取,更多精彩陆续奉上。

你可能感兴趣的:(Java坑人面试题系列: 线程/线程池(高级难度))