当前位置: 首页 > 科技观察

我依靠(呼唤),我的未来(Future)在哪里???

时间:2023-03-20 23:43:18 科技观察

本文由读者投稿,和你一起聊聊Future~我们都知道Java中创建线程的方式主要有3种:继承Thread类;实现Runnable接口;实现Callable接口。后两者的区别在于Callable接口中的call()方法可以异步返回一个计算结果Future,一般需要配合ExecutorService来执行。这组操作在代码实现上看起来并不难,但是不太清楚call()方法是如何执行的(通过ExecutorService),Future的结果是如何得到的。那么本文我们来了解一下Callable接口和Future的使用,主要针对两个问题:承载具体任务的call()方法是如何执行的?如何获取任务的执行结果?你可能会说,这两个不是问题吗?任务执行后会有返回结果,返回结果必须在任务执行后返回。它可以返回另一个任务的结果吗?别着急,耐心看完,你会发现这两个真的是个问题。本文将分为两部分。第一部分介绍了JavaAPI中task、execution、result这三个概念的实体及其各自的继承关系。第二部分通过一个简单的例子回顾一下它们的用法,然后理解这两个问题的答案。由于Callable、Executor、Future都是执行并返回结果的任务,所以我们先来看具体的任务,即Callable接口。Task:Callable很简单,只包含一个带有泛型“返回值”的call()方法,最后需要返回定义类型的结果。如果任务没有结果可返回,则将泛型V设置为void并返回null;应该做的伎俩。与Runnable相比,另一个明显的区别是Callable可以抛出异常。publicinterfaceCallable{Vcall()throwsException;}publicinterfaceRunnable{publicabstractvoidrun();}Execution:ExecutorService说到线程就离不开线程池,说到线程池就一定离不开Executor接口。下图是Executor的框架。我们常用的两个具体实现类ThreadPoolExecutor和ScheduledThreadPoolExecutor,是通过Executors类中的静态方法获取的。Executors包括线程池和线程工厂的构建,与Executor接口的关系类似于Collection接口与Collections类的关系。那么我们就从上到下了解一下Executor框架,学习学习任务是如何执行的。首先是Executor接口,其中只定义了execute()方法。publicinterfaceExecutor{voidexecute(Runnablecommand);}ExecutorService接口继承了Executor接口,主要扩展了一系列的submit()方法以及执行器的终止和判断状态。采取第一个Future提交(可调用任务);举个例子,其中task是用户定义的异步任务,Future代表任务的执行结果,泛型T代表任务结果的类型。publicinterfaceExecutorServiceextendsExecutor{voidshutdown();//现有任务完成后停止线程池ListshutdownNow();//立即停止线程池booleanisShutdown();//判断是否已经停止booleanisTerminated();Futuresubmit(Callabletask);//提交CallaletaskFuturesubmit(Runnabletask,Tresult);Futuresubmit(Runnabletask);//InvokeAll()andCallable集合的其他方法}抽象类AbstractExecutorService是ThreadPoolExecutor的基类。在下面的代码中,它实现了ExecutorService接口中的submit()方法。注释中是对应的newTaskFor()方法的代码,很简单,就是将传入的Callable或Runnable参数封装成一个FutureTask对象。//1。第一个重载方法,参数为CallablepublicFuturesubmit(Callabletask){if(task==null)thrownewNullPointerException();RunnableFutureftask=newTaskFor(task);//returnnewFutureTask(可调用);执行(ftask);returnftask;}//2。第二种重载方法,参数为RunnablepublicFuturesubmit(Runnabletask){if(task==null)thrownewNullPointerException();RunnableFutureftask=newTaskFor(task,null);//returnnewFutureTask(task,空);执行(ftask);returnftask;}//3。第三种重载方法,参数为Runnable+returnobjectpublicFuturesubmit(Runnabletask,Tresult){if(task==null)thrownewNullPointerException();RunnableFutureftask=newTaskFor(task,result);//returnnewFutureTask(task,result);execute(ftask);returnftask;}也就是说无论传入的是Callable还是Runnable,submit()方法实际上做了三件事。具体来说,submit()首先生成一个RunnableFuture引用FutureTask实例,然后调用execute()方法执行,那么我们可以推测FutureTask继承自RunnableFuture,而RunnableFuture实现了Runnable,因为execute()的参数应该是RunnFutureTask的构造函数也涉及到able类型,我们来看一下。publicFutureTask(Callablecallable){this.callable=callable;this.state=NEW;}publicFutureTask(Runnablerunnable,Vresult){this.callable=Executors.callable(runnable,result);//通过适配器调用runnable()和returnresultthis.state=NEW;}FutureTask有两种构造方法。第一个构造方法比较简单,对应上面的第一个submit(),对Callable进行组合封装,并设置state为NEW;而第二种构造方法对应的是上面最后两个submit()重载,不同的是Executors.callable是先将Runnable和result组合成Callable,而这里的适配器RunnableAdapterimplementsCallable是用来在call()并返回结果。staticfinalclassRunnableAdapterimplementsCallable{finalRunnabletask;finalTresult;//返回的结果;显然:你需要在run(){this.task=task;this.result=result;}publicTcall(){task.run();returnresult;}}适配器设计模式中分配RunnableAdapter(Runnabletask,Tresult),它通常包括三类角色:目标接口Target、适配器Adapter和适配器Adaptee,其中目标接口代表客户端(当前业务系统)需要的功能,通常是借口或抽象类;适配对象是现有类,不能满足使用要求;adapter是一个converter,也叫wrapper,用来给adaptee添加target功能,让client可以按照target接口的格式正确访问。对于RunnableAdapter,Callable是它的目标接口,Runnable是被适配者。RunnableAdapter重写了call()方法,这样就可以根据Callable的要求来使用了。同时它的构造方法接收适配器和目标对象,满足了call()方法有返回值的要求。所以总结一下submit()方法的执行过程就是:“将Callable封装在Runnable的一个子类中,传递给execute()执行”。Result:说Future是一个异步任务的执行结果是不准确的,因为它代表了一个任务的执行过程,有状态,可以取消,get()方法的返回值就是result的任务。publicinterfaceFuture{booleancancel(booleanmayInterruptIfRunning);booleanisCancelled();booleanisDone();Vget()throwsInterruptedException,ExecutionException;Vget(longtimeout,TimeUnitunit)throwsInterruptedException,ExecutionException,TimeoutFutureException;}我们上面也提到了。从官方的注释来看,RuunableFuture是一个可以运行的future。它实现了Runnable和Future这两个接口。在run()方法中进行计算时,需要保存结果,以便通过get()获取。publicinterfaceRunnableFutureextendsRunnable,Future{/***SetsthisFuturetotheresultofitscomputationunlessithasbeencancelled.*/voidrun();}FutureTask直接实现RunnableFuture接口。作为执行过程,有以下几种状态,其中COMPLETING是一个临时状态,表示正在设置结果或异常。相应的,设置完成后,状态变为NORMAL或EXCEPTIONAL;CANCELLED和INTERRUPTED表示任务被取消或中断。在上面的构造函数中,将状态初始化为NEW。私有易失性int状态;privatestaticfinalintNEW=0;privatestaticfinalintCOMPLETING=1;注意outcome的注释,不管是否发生异常,都会返回这个outcome,因为在执行的时候,如果执行成功,就把result设置给它(set()),当发生异常的时候,异常赋值给他(setException()),得到结果时也返回结果(通过report())。publicclassFutureTaskimplementsRunnableFuture{privateCallablecallable;//目标,要执行的任务/**保存执行结果或异常,在get()方法中返回/抛出*/privateObjectoutcome;//非-volatile,通过CAS保证线程安全publicvoidrun(){......Callablec=callable;if(c!=null&&state==NEW){Vresult;booleanran;try{result=c.call();//调用()执行用户任务并得到结果ran=true;//执行完成,将ran设置为true}catch(Throwableex){//调用call()时出现异常,而run()方法继续执行result=null;ran=false;setException(ex);//setException(Throwablet):compareAndSwapInt(NEW,COMPLETING);outcome=t;}if(ran)set(result);//set(Vv):compareAndSwapInt(NEW,COMPLETING);outcome=v;}}publicVget()throwsInterruptedException,ExecutionException{ints=state;if(s<=COMPLETING)s=awaitDone(false,0L);//加入队列等待COMPLETING完成,响应超时,interruptreturnreport(s);}publicVget(longtimeout,TimeUnitunit)throwsInterruptedException,ExecutionException,TimeoutException{//超时等待}privateVreport(ints)throwsExecutionException{Objectx=outcome;if(s==NORMAL)//返回outcome作为执行结果return(V)x;如果(s>=CANCELLED)thrownewCancellationException();thrownewExecutionException((Throwable)x);//将结果作为捕获返回}}FutureTask实现了RunnableFuture接口,所以它有两个函数。首先将execute()方法作为Runnable传入执行,同时封装Callable对象,在run()中调用其c??all()方法;其次,作为Future管理任务的执行状态,将call()的返回值保存在outcome中,通过get()获取。这好像能回答前两个问题,很自然,好像是一道题,只不过当异常发生时,返回的不是任务的结果,而是异常对象。总结一下继承关系:2.文章标题有点唬人。毕竟是关于Callable的用法。现在我们知道Future代表任务执行的过程和结果,作为call()方法的返回值获取执行结果;而FutureTask是一个RunnableFuture,它既是任务执行的过程和结果,也是最终执行调用方法的载体。下面举个例子来看看它们在使用上的区别。首先创建一个任务,即定义一个任务类来实现Callable接口,在call()方法中加入我们的操作。这里用三秒返回100来模拟计算过程。classMyTaskimplementsCallable{@OverridepublicIntegercall()throwsException{System.out.println("子线程开始计算...");for(inti=0;i<3;++i){Thread.sleep(1000);System.out.println("子线程计算,耗时"+(i+1)+"秒");}System.out.println("子线程计算完成,返回:100");return100;}}那么好吧,创建一个线程池,并实例化一个MyTask备用。ExecutorServiceexecutor=Executors.newCachedThreadPool();MyTasktask=newMyTask();现在分别使用Future和FutureTask来获取执行结果,看看它们之间的区别。使用FutureFuture一般作为submit()的返回值,在主线程中以阻塞的方式获取异步任务的执行结果。System.out.println("主线程启动线程池");Futurefuture=executor.submit(task);System.out.println("主线程获取返回结果:"+future.get());executor.shutdown();看输出结果:主线程启动线程池,子线程开始计算...子线程计算过程中,子线程计算需要1秒,需要2秒计算子线程,3秒完成子线程计算,返回:100主线程得到返回结果:100主线程启动线程池子线程开始计算...子线程-线程计算耗时1秒,子线程计算耗时2秒,子线程计算耗时3秒,子线程计算完成,返回:100主线程获取返回结果:100自get()方法阻塞获取结果,输出顺序为子线程计算完成后主线程输出结果。使用FutureTask由于FutureTask集成了“任务和结果”,我们可以使用FutureTask本身而不是返回值来管理任务。这需要首先使用Callable对象构造FutureTasks并调用不同的submit()重载方法。System.out.println("主线程启动线程池");FutureTaskfutureTask=newFutureTask<>(task);executor.submit(futureTask);//作为Ruunable传入submit()System.out.println("主线程获取返回结果:"+futureTask.get());//获取结果为Futureexecutor.shutdown();这个程序的输出和上面的完全一样,其实两者在实际执行中的差别也不大,虽然前者调用了submit(Callabletask)而后者调用了submit(Runnabletask),但是在最后,通过execute(futuretask)将任务添加到线程池中。综上所述,费了很大的功夫,尽可能详细的解释了Callable中的任务是如何执行的。总结就是:在线程池中,submit()方法实际上是将Callable封装在FutureTask中,作为Runnable的child来使用。该类被传递给execute()以进行真正的执行;FutureTask在run()中调用Callable对象的call()方法并接收返回值或捕获异常保存在Objectoutcome中,并在执行过程中管理state状态;FutureTask也作为Future的子类,通过get()返回任务的执行结果,如果没有执行完成,则会阻塞,通过等待队列等待完成;FutureTask是一个RunnableFuture,最重要的两个方法如下。本文转载自微信公众号“JavaBuilder”,可通过以下二维码关注。如需转载本文,请联系Java开发者公众号。