面试题20解析-Executor框架(下)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 题目:请说一下Executor框架下有几种线程池,及它们之间的区别?

一关于Future


在java中创建一个线程绝非难事,从JDK1.0开始后,调用者只需直接继承Thread类的实现,或者实现Runnable接口,便可以创建一个运行线程。但是这两种线程创建方式无法将运行结果返回给调用者,因此从JDK 1.5以后,提供了Callable与Future,是调用者可以获得任务执行完成后返回的结果。


二 Runnable 与 Callable


我们先来看一下接口Runnable与Callable在java.lang.*包下的声明(JDK8):


public interface Runnable {
    public abstract void run();
}
public interface Callable<V> {
    V call() throws Exception;
}

对比两个接口的声明,我们可以看到Runnable.run()没有任何返回,并且不会抛出异常,而Callable.call()接口会返回一个泛型变量V,而V的类型正是调用者传递进来的类型,在call()计算过程无法正常完成时,会抛出异常。在Executor框架下,我们可以通过ExecutorService.submit()来向线程池提交一个Callable对象,这时submit()的返回对象便是Future对象,也就是说Future对象可以获取到Callable.call的返回值,ExecutorService.submit()声明如下:


/

<T> Future<T> submit(Callable<T> task);



Future的应用场景


关于Future的使用,我们考虑这样的实际场景,系统启动需要从远程获取系统的加载资源,假设我们通过以下函数实现需求:

private Resource getResourceFromRemote();

此时主线程会一直阻塞在getResourceFromRemote(),等待系统从远程获取到加载资源,才能继续系统的启动过程。如果该过程比较长,系统还有其他耗时的模块需要加载,并且这些模块并不依赖于远程的这些加载资源,此时系统阻塞在getResourceFromRemote(),无疑会增加系统的启动时间,是一种性能浪费。那么如何改进这一问题?最容易想到的策略就是,在系统等待远程资源的同时,同时加载其他模块,等到系统的加载后期需要用到资源时,再继续等待远程资源的获取,从而系统中相互无依赖的加载模块能并发加载,提高系统的加载速度。如下图所示,系统可以在获取远程资源的同时实现模块1和模块2的加载:


在具体实现中,我们就可以通Future与Callable将getResourceFromRemote封装成异步任务,实现如下:


private static final ExecutorService threadpool = Executors.newFixedThreadPool(3);
private static Future<Resource> getResourceFromRemoteSync(){
    Future<Resource> future = threadpool.submit(
            new Callable<Resource>() {
                public Resource call() throws Exception {
                    return getResource();
                }
            }
    );
    return future;
}

主线程只需通过Future.get()方法,便可以获取到资源获取任务的执行结果,系统的启动过程伪代码如下:


//step 1:开始获取远程资源
Future<Resource> future = getResourceFromRemoteSync();  
//step 2:进行其他模块的加载工作,这里省略
//...
//step 3:等待获取远程资源的任务返回的资源
Resource r = future.get(); 


通过这个例子,可以看出Future对象本身是一个显式的引用,一个对异步处理结果的引用,正如Future的英文的意义未来一样,Future的引用指向的是计算任务完成后产生的结果,而并非当下就需要的结果。在使用异步资源加载的系统设计中,还有一种重要的机制可以实现上述需求,回调机制。回调是一种常见的异步并发模式,它有即时性高、接口设计简单等有点。但相对于Future,其缺点也非常明显。首先,多线程环境下的回调一般是在触发回调的模块线程中执行的,这就意味着编写回调方法时通常必须考虑线程互斥问题;其次,回调方式接口的提供者在本模块的线程中执行用户应用的回调也是相对不安全的,因为你无法确定它会花费多长时间或出现什么异常,从而可能间接导致本模块的即时性和可靠性受影响;再者,使用回调接口不利于顺序流程的开发,因为回调方法的执行是孤立的,要与正常流程汇合是比较困难的。因此回调接口适合于在回调中只需要完成简单任务,并且不必与其它流程汇合的场景。


四  Future接口


在我们初步了解了Future的特性与使用场景后,我们来详细看一下Future接口的具体功能,Future声明如下(JDK 8):


public interface Future<V> {
   boolean cancel(boolean mayInterruptIfRunning);
   boolean isCancelled();
   boolean isDone();
   V get() throws InterruptedException, ExecutionException;
   V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}


可以看到Future提供了5个接口,get()接口我们已经用过了,表示对异步计算结果的获取,此外get()接口还重载了支持超时等待的接口,该接口如果在超时时间内未获取到计算结果,将会抛出TimeoutException异常,这样就能确保在计算任务长时间不返回时,可能造成程序僵死的情况。其他接口的功能与意义如下:


  • cancel():用来取消任务的执行,起返回值表示是否成功取消任务。参数mayInterruptIfRunning的设置表示是否允许取消还未完成的任务,如果任务已经完成,那么无论mayInterruptIfRunning设置成true或false,cancel()都会返回false;如果任务正在执行,那么mayInterruptIfRunning设置为true,任务将会被取消,并且cancel()返回true,而如果mayInterruptIfRunning设置为false,任务将无法被取消,任务还将继续执行,cancel()返回false;如果任务还未执行,那么无论mayInterruptIfRunning设置为什么,任务都会被成功取消,所以cancel()也会返回true。可能这个接口的状态与参数关系比较复杂,关系如下表所示:

image.png


  • 也就是说,只有cancel()方法成功取消了任务的执行,才会返回true。



  • isCancelled():表示在任务执行完成之前,任务是否被取消。
  • isDone():表示任务是否已经结束,无论是任务是正常结束,抛出异常还是被取消结束,都会返回true。


Future的5个接口为使用者提供了灵活的其同步或异步控制的发挥空间,开发者可以根据流程的需要自由决定是否需要等待(Future.isDone()),何时等待(Future.get())以及等待多久(Future.get(timeout))。


关于FutureTask


Future只是一个接口,一个接口是不能直接使用的,必须有具体类来实现,在Executor框架下实现Future接口的具体类就是FutureTask。通过ExecutorService.submit()接口提交的任务都会被封装成一个FutureTask对象,ExecutorService.submit()的实现如下:


public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}

即使调用者通过submit()接口提交的是Runnable类型的任务,在FutureTask构造函数中也会使用Executors.callable(Runnable, V)方法,将Runnable与返回结果封装成一个callable对象。

public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

从上面的分析,可以看出Executor框架下的Future特性的支持,是由FutureTask实现的。那么FutureTask的实现细节又如何呢?

从FutureTask类的源码可以看到FutureTask实现了RunnableFuture接口。


public class FutureTask<V> implements RunnableFuture<V> {
       private Callable<V> callable;
       public FutureTask(Callable<V> callable) {
             this.callable = callable;
      }
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
      void run();
}


由上,可以看到FutureTask的构造函数接受一个Callable类型的任务,并且实现了Runnable和Future接口,这样FutureTask既可以通过Thread包装来直接执行,也可以提交给ExecuteService来执行。所以FutureTask既是Future、Runnable,又是包装了的Callable(如果是Runnable最终也会被转换为Callable ), 它是这两者的合体。


总结

summary

通过本节,我们了解了Executor框架下对于Future特性的支持是由FutrueTask实现的,在实际的生产环境中使用Futrue往往能将结构复杂的代码实现整理成具有清晰流程的可读性强的代码,Futrue对我们来说更是一种JDK为我们提供的并发编程机制,使得我们队线程的执行控制力更强,更灵活。






相关文章
|
10天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
27 3
|
17天前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
75 3
|
2天前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
13 1
|
24天前
|
Web App开发 IDE 测试技术
自动化测试的利器:Selenium 框架深度解析
【10月更文挑战第2天】在软件开发的海洋中,自动化测试犹如一艘救生艇,让质量保证的过程更加高效与精准。本文将深入探索Selenium这一强大的自动化测试框架,从其架构到实际应用,带领读者领略自动化测试的魅力和力量。通过直观的示例和清晰的步骤,我们将一起学习如何利用Selenium来提升软件测试的效率和覆盖率。
|
23天前
|
人工智能 缓存 Java
深入解析Spring AI框架:在Java应用中实现智能化交互的关键
【10月更文挑战第12天】Spring AI 是 Spring 框架家族的新成员,旨在满足 Java 应用程序对人工智能集成的需求。它支持自然语言处理、图像识别等多种 AI 技术,并提供与云服务(如 OpenAI、Azure Cognitive Services)及本地模型的无缝集成。通过简单的配置和编码,开发者可轻松实现 AI 功能,同时应对模型切换、数据安全及性能优化等挑战。
|
13天前
|
分布式计算 Java 应用服务中间件
NettyIO框架的深度技术解析与实战
【10月更文挑战第13天】Netty是一个异步事件驱动的网络应用程序框架,由JBOSS提供,现已成为Github上的独立项目。
28 0
|
24天前
|
JSON 应用服务中间件 API
使用 Gin 框架实现文件上传:机制与深入解析
使用 Gin 框架实现文件上传:机制与深入解析
|
19天前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
39 0
|
19天前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
29 0
|
19天前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
33 0

推荐镜像

更多