Java异步并发Callable与Runable

时间:2022-12-07 17:57:27

Future到底是什么东西?很多人都对这个东西感到特别奇怪(好吧,我承认,那个很多人就只是我自己而已),就我现在的理解,因为本人在并发这方面没有多少实践经验,所以只好就着一些资料和自己的理解给它下个定义,Future就是保存我们任务的完成信息,比如说,任务中会通过返回某些东西告诉别人它已经结束了,而Future中就保存了这种信息。利用Futu保存和得到任务的结果的用法如下: 

Java异步并发Callable与Runable
      Future<String> future = threadPool.submit(new Callable<String>(){
           @Override
           public String call() throws Exception{
                Thread.sleep(3000);
                return "future";
           }
        try{
             System.out.println("waiting.....");
             System.out.println(future.get());
        }catch(InterruptedException e){
              e.printStackTrace();
        }catch(ExecutionException e){
              e.printStackTrace();
        }
Java异步并发Callable与Runable

          注意到没?代码中的System.out.println(future.get())这一句,就是返回任务的结果,而任务的结果是保存在Future<String>中的,相信大家都有注意到,        

     Future<String> future = threadPool.submit(new Callable<String>(){
       @Override
        public String call() throws Exception{}
       }

 

         Callable相当于Runnable,所以,这里实现的是一个线程,但是与Runnable不同的是,它是具有返回值的,这个返回值就是我们想要任务返回的结果,比如说,我们想要任务返回的是一个提示信息,那么,返回值可以是String,然后在我们要实现的call()方法中return一句提示信息,接着只要使用Future类的get()方法,就可以从里面得到提示信息了,只要任务完成。所以,由此我们可以知道,java SE5比起以前来,在并发这方面做了更多的工作,它完善了我们的并发线程机制,使我们可以更好的根据任务的完成情况来进行与其他任务的协作,比如说,我们可以通过Future的返回值来决定是否终止任务,或者开启另一个任务。任务的终止可以使用Future的方法future.cancel(boolean),其中boolean为true或false,来决定是否终止,至于开启另一个任务,可以重新开启另一个线程,但是这里就马上有个问题浮现出来,就是当Futrue的结果返回来时,该任务有没有结束呢?因为这时一定已经执行完该任务的call()方法。是的,该任务已经结束了,只是我们没有取出它的返回结果而已。

        看到上面,相信大家一定都对Future的新特性产生非常浓厚的兴趣,非常想要将这个新玩意儿马上运用起来,但是,且慢,每次在遇到这种新东西的时候,我们都会有一个念头,那就是我们有必要使用吗?如果旧的东西已经足够用了,那为什么还要用多余的方法呢?是的,这种想法是对的,因为我们的程序设计原则都是能够尽量简单则尽量简单,但是Future是一个接口,一个泛型接口,适合各种返回值的情况,而且这个接口提供了很多有用的方法,再加上,我们永远无法知道我们的代码以后到底会变成怎么样子,是否需要添加新的功能等等,而这些,如果一开始使用的是旧的东西的话,添加新的东西,那么,我们就要对我们的代码进行修改,但是,我是这么认为的,就目前而言,Thread修改为Future并不是很难,所以,这方面倒是没有多大顾虑,熟悉啥就用啥,至少都要了解,因为我们在写代码时,更多时间里是在阅读别人的代码,如果别人使用的代码是使用以前的接口的话,而且这种情况是非常常见的,所以,我们必须要看得懂代码并且能够将其转化为我们的新接口,这些就需要我们能够对其有一定的了解,并且明白它们之间的联系和区别。所以,接下来就是介绍一下新接口的一些方法以便我们能够更好的使用新接口。

    A、boolean cancel(Boolean mayInterruptlfRunning):试图取消该Future里关联的Callable任务
    B、<?> get()  throws InterruptedException, ExecutionException :返回Callable任务里的call方法的返回值,调用该方法将导致线程阻塞,必须等到子线程结束才得到返回值,但是并不会妨碍其他任务的执行。线程被中断会抛出InterruptedException异常,任务执行中出现错误会抛出ExecutionException异常,如果任务被取消,还会抛出CancellationException 异常。
    C、<?> get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException :返回Callable任务里的call方法的返回值,该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException,其他与上面的get()方法用法一样。
    D、boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true。
    E、boolean isDone():如果Callable任务已经完成,则返回true,而且这里要注意,如果任务正常终止、异常或取消,那么都将会返回true.
    关于Future的使用,其实重点并不在于我们对它的方法到底有多么熟悉,因为我们完全可以通过查找文档来了解它们的用法,那么我们需要掌握的是什么呢?就是通过利用Future,我们可以做到什么?它解决的主要问题到底是什么?这些都是有利于我们对它的掌握。很多人都喜欢研究接口的内部实现,是的,这个很重要,因为想要了解接口的实现,知道它的实现细节是非常重要的,只要知道它的实现细节,那么我上面的问题其实也就可以马上得到答案。但是我并不是什么大师级人物,所以像是这种枯燥的东西完全没有信心可以讲得通俗易懂,自己没有晕都不错了!所以,就只能通过一些用例及其代码来研究研究,如果是非要知道底细的读者,还请自己详细阅读一下相关文档。
       那么,进入正题,既然我们知道Future是一个接口,那么,就一定有一个具体的类的实现,那么这个具体的类是什么呢?就是FutureTask.FutureTask的作用非常大,甚至可以说是使用Future的灵魂,因为就是它用来包装Callable对象的,所以,很多方法都是通过它来实现的。
      代码如:
      
Java异步并发Callable与Runable
ExecutorService executor = Executors.newSingleThreadExecutor();   
FutureTask<String> future =   
       new FutureTask<String>(new Callable<String>() {//使用Callable接口作为构造参数   
         public String call() {   
           //真正的任务在这里执行,这里的返回值类型为String,可以为任意类型   
       }});   
executor.execute(future);   
//在这里可以做别的任何事情   
try {   
    result = future.get(5000, TimeUnit.MILLISECONDS); //取得结果,同时设置超时执行时间为5秒。同样可以用future.get(),不设置执行超时时间取得结果   
} catch (InterruptedException e) {   
    futureTask.cancel(true);   
} catch (ExecutionException e) {   
    futureTask.cancel(true);   
} catch (TimeoutException e) {   
    futureTask.cancel(true);   
} finally {   
    executor.shutdown();   
}  
Java异步并发Callable与Runable

      这里就是FutureTask的一般用法,它最大的好处就是我们可以将任务交给执行器后执行其他操作,然后再从里面得到任务的结果。这里必须要注意,只有FutureTask这种既实现Runnable又实现Callable才能够通过executor()递交给ExecutorService,而Future不行,只能通过submit(),因为executor()要求的参数是一个实现了Runnable的类。如果我们不想要直接构造Future对象,那么我们可以这样写:

      

Java异步并发Callable与Runable
ExecutorService executor = Executors.newSingleThreadExecutor();   
FutureTask<String> future = executor.submit(   
   new Callable<String>() {//使用Callable接口作为构造参数   
       public String call() {   
      //真正的任务在这里执行,这里的返回值类型为String,可以为任意类型   
   }});   
//在这里可以做别的任何事情   
//同上面取得结果的代码  
Java异步并发Callable与Runable

       这里是使用ExecutorService.submit方法来获得Future对象,submit方法既支持Callable接口类型,也支持Runnable接口作为参数,具有很大的灵活性,而且所有的submit()方法都会返回一个Future值,无论是Runnable还是Callable。上面两种方法哪种比较好?其实都一样,只是第二种的话,可以在定义FutureTask的同时就将FutureTask提交给Executor。个人的话,比较倾向于第二种,因为我们的代码如果在不影响阅读性的基础上能够越简单越好,哪怕是一句代码。

       Future中的call()方法相比run()方法更加强大,除了上面说的可以具有返回值外,相信大家在上面的代码中也可以看到,call()方法是可以声明异常的,这样,就能省去run()方法的异常处理。
   创建并启动有返回值的线程的步骤如下:
   一、创建Callable接口的实现类,并实现call方法,该call方法的返回值,并作为线程的执行体。
   二、创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call方法的返回值
   三、使用FutureTask对象作为Thread对象的target创建、并启动新线程
   四、调用FutureTask对象的方法来获得子线程执行结束后的返回值
    FutureTask还实现了Runnable接口,所以可以直接交给Executor执行,正如上面的代码所演示的。这种好处就非常明显,因为我们还是可以将我们的线程交给Executor来管理,不用学习新的线程管理机制。我们都知道,Executor框架的实现基本上是用Runnable,但是Runnable的能力还是相当有限,它不能返回一个值,也不能抛出异常,更重要的是,对于复杂费时的计算根本难以处理。为什么这么说呢?那是因为Runnable并没有方法可以支持任务超时。什么是任务超时?就是我们允许这个任务的执行要多久后才返回结果,如果这个任务在指定的时间内没有完成,那么就会抛出异常,那就是可以设置超时的get()方法。但是我们知道,Runnable中并没有与get()类似的方法,我们最多能做的,只是让线程睡眠。
      至此我们就可以知道,Future大概是怎样的东西。其实,它的名字含义就已经说明一切,Future,未来的意思,说明我们通过Future是为了应付未来的问题。什么是未来的问题,这个就是它所解决的问题,超时问题,以前,我嫩得到任务的结果是即时性的,但是现在可以有一个时间上的缓冲,可以在这段等待的时间内执行其他的动作,这样我们的并发设计就能更加灵活,而且还能更好的应付现实生活中的实际问题,因为很多问题都是不能马上就能解决并且返回结果的。这就是Future的长处,异步处理,但是Future又不同于一般的异步处理机制,它也可以选择同步,所以灵活性更大。这点怎么做到的呢?取决于它里面的方法。比如说,我们可以根据流程的需要决定是否需要等待(Future.isDone()),何时等待(Future.get()),等待多久(Future.get(timeout)),还可以根据它的返回值判断数据是否就绪而决定要不要借这个空档完成其它任务。 
       现实生活中的并发问题非常多而且要求又不尽相同,所以,自然,Future也有很多相应的衍生形态。下面就只是介绍几种常见的,因为能力有限,加上这方面的工作又几乎没有,所以,如果是想要更加详细的内容,还请查阅相关资料。
       1.Lazy Future
       与一般的Future不同,Lazy Future在创建之初不会主动开始准备引用的对象,而是等到请求对象时才开始相应的工作。因此,Lazy Future本身并不是为了实现并发,而是以节约不必要的运算资源为出发点,效果上与Lambda/Closure类似。例如设计某些API时,你可能需要返回一组信息,而其中某些信息的计算可能会耗费可观的资源。但调用者不一定都关心所有的这些信息,因此将那些需要耗费较多资源的对象以Lazy Future的形式提供,可以在调用者不需要用到特定的信息时节省资源。
另外Lazy Future也可以用于避免过早的获取或锁定资源而产生的不必要的互斥,因为并没有准备对象。
       2.Promise
       Promise可以看作是Future的一个特殊分支,常见的Future一般是由服务调用者直接触发异步处理流程,比如调用服务时立即触发处理或 Lazy Future的取值时触发处理。但Promise则用于显式表示那些异步流程并不直接由服务调用者触发的情景。例如Future接口的定时控制,其异步流程不是由调用者,而是由系统时钟触发,再比如淘宝的分布式订阅框架提供的Future式订阅接口,其等待数据的可用性不是由订阅者决定,而在于发布者何时发布或更新数据。因此,相对于标准的Future,Promise接口一般会多出一个set()或fulfill()接口。
        3.复用式的Future
        常规的Future是一次性的,也就是说当你获得了异步的处理结果后,Future对象本身就失去意义了。但经过特殊设计的Future也可以实现复用,这对于可多次变更的数据显得非常有用。例如前面提到的淘宝分布式订阅框架所提供的Future式接口,它允许多次调用waitNext()方法(相当于Future.get()),每次调用时是否阻塞取决于在上次调用后是否又有数据发布,如果尚无更新,则阻塞直到下一次的数据发布。这样设计的好处是,接口的使用者可以在其任何合适的时机,或者直接简单的在独立的线程中通过一个无限循环响应订阅数据的变化,同时还可兼顾其它定时任务,甚至同时等待多个Future。简化的例子如下:

      

Java异步并发Callable与Runable
for (;;) {  
  schedule = getNextScheduledTaskTime();  
  while(schedule > now()) {  
    try {  
      data = subscription.waitNext(schedule - now());  
      processData(data);  
    } catch(Exception e) {...}  
  }  
  doScheduledTask();  
}  
Java异步并发Callable与Runable