Bootstrap

Future和Callback

1 Future和Callback

简单来说,Future代表着一个异步任务在未来的执行结果,这个结果可以在最终的某个时间节点通过Future的get方法来获得,关于Future更多的细节和原理,可参考多线程设计模式:Future设计模式

对于长时间运行的任务来说,使其异步执行并立即返回一个Future接口是一种比较不错的选择,因为这样可以允许程序在等待结果的同时继续去执行其他的任务

Future接口也是在JDK1.5版本中随着并发包一起被引入JDK的,Future接口的定义如下所示

public interface Future<V> {

   /**
    * 取消任务的执行,
    * 如果mayInterruptIfRunning为true,则工作线程将会被中断,否则即使执行了cancel方法,也会等待其完成,
    * 无论mayInterruptIfRunning为true还是false,isCancelled()都会为true,并且执行get 方法会抛异常
    */
    boolean cancel(boolean mayInterruptIfRunning);

    /**
    *判断异步任务是否被取消
    */
    boolean isCancelled();

   /**
    * 判断异步任务的执行是否结束
    */
    boolean isDone();

   /**
    * 获取异步任务的执行结果,如果任务未运行结束,则该方法会使当前线程阻塞
    * 异步任务运行错误,调用get方法会抛出ExecutionException异常
    */
    V get() throws InterruptedException, ExecutionException;

	// 同get方法,但是允许设置最大超时时间
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

接下来看看Callback接口,该接口与Runnable接口非常相似,但是Runnable作为任务接口最大的问题就是无法返回最终的计算结果,因此在JDK1.5版本中引入了Callable泛型接口,它允许任务执行结束后返回结果。

package java.util.concurrent;

@FunctionalInterface
// 泛型接口
public interface Callable<V>
{
    V call() throws Exception;
}

1.1 快速认识

 public static void main(String[] args) throws ExecutionException, InterruptedException {
     ExecutorService executorService = Executors.newSingleThreadExecutor();
     Future<String> future = executorService.submit(() -> {
         try {
             TimeUnit.SECONDS.sleep(10);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         return "hello";
     });
     // 此时会阻塞,大概等10s,上面的线程执行结束后,就会返回
     future.get();
     // 线程执行的返回值:hello
     System.out.println("线程执行的返回值:" + future.get());
 }

1.2 取消任务

  • 取消异步正在执行的任务:如果一个异步任务的运行特别耗时,那么Future是允许对其进行取消操作的。

调用cancel方法

public static void main(String[] args) throws ExecutionException, InterruptedException {
   ExecutorService executorService = Executors.newSingleThreadExecutor();
   Future<String> future = executorService.submit(() -> {
       try {
           TimeUnit.SECONDS.sleep(10);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       return "hello";
   });
   // 等一下,让任务执行起来
  = TimeUnit.SECONDS.sleep(2);
   // 取消正在执行的异步任务(参数为false,任务虽然已被取消但是不会将其中断)
   future.cancel(false);
   System.out.println(future.isCancelled()); // true
   // 但是调用get方法会抛出异常
   future.get();
}

如果在执行future的cancel方法时指定参数为true,那么在callable接口中正在运行的可中断方法会被立即中断,比如sleep方法:

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    Future<String> future = executorService.submit(() -> {
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            System.out.println("中断任务");
        }
        return "hello";
    });
    // 等一下,让任务执行起来
    TimeUnit.SECONDS.sleep(2);
    // 取消正在执行的异步任务(true,任务会将其中断)
    future.cancel(true);
    System.out.println(future.isCancelled()); // true

}
true
中断任务

1.3 异常捕获

Runnable类型的任务中,run()方法抛出的异常(运行时异常)只能被运行它的线程捕获(有可能会导致运行线程死亡),但是启动运行线程的主线程却很难获得Runnable任务运行时出现的异常信息。,我们可以通过设置UncaughtExceptionHandler的方式来捕获异常,但是这种方式的确不够优雅,并且也无法精确地知道是执行哪个任务时出现的错误,Future则是通过捕获get方法异常的方式来获取异步任务执行的错误信息的

通过设置UncaughtExceptionHandler的方式来捕获异常,可参考Hook线程和捕获线程执行异常

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    Future<String> future = executorService.submit(() -> {
        throw new RuntimeException("运行异常。。。");
    });
    // 等一下,让任务执行起来
    TimeUnit.SECONDS.sleep(2);
    try {
        future.get();
    } catch (ExecutionException e) {
        // 这里就可以捕获到线程运行出现的异常信息
        System.out.println(e.getClass().getSimpleName() + " -> " + e.getMessage());
    }
}

运行输出:

ExecutionException -> java.lang.RuntimeException: 运行异常。。。

2 ExecutorService与Future

线程池中通过submit方法提交一个callable异步执行任务并且返回future的操作。线程池中还提供了其他更多任务执行的方法

2.1 提交Runnable类型任务

Submit方法除了可以提交执行Callable类型的任务之外,还可以提交Runnable类型的任务并且有两种重载形式

/**
提交Runnable类型的任务并且返回Future,待任务执行结束后,
通过该future的get方法返回的结果始终为null。
**/
public Future<?> submit(Runnable task);

/**
上面的提交Runnable类型的任务虽然会返回Future,
但是任务结束之后通过future却拿不到任务的执行结果,
而通过该submit方法则可以。
**/
public <T> Future<T> submit(Runnable task, T result)

2.2 invokeAny

ExecutorService允许一次性提交一批任务,但是其只关心第一个完成的任务和结果,比如,我们要获取某城市当天天气情况的服务信息,在该服务中,我们需要调用不同的服务提供商接口,最快返回的那条数据将会是显示在APP或者Web前端的天气情况信息,这样做的好处是可以提高系统响应速度,提升用户体验

invokeAny是一个阻塞方法,它会一直等待直到有一个任务完成

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    List<Callable<Integer>> callables = new ArrayList<>();
    // 定义10 Callable
    for (int i = 0; i < 10; i++) {
        callables.add(() -> {
            int random = ThreadLocalRandom.current().nextInt(30);
            // 随机休眠,模拟不同接口访问的不同时间开销
            TimeUnit.SECONDS.sleep(random);
            System.out.println("Task: " + random + " completed in Thread " + currentThread().getName());
            return random;
        });
    }
    // 执行
    try {
        Integer res = executorService.invokeAny(callables);
        System.out.println("result: " + res);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

输出

Task: 3 completed in Thread pool-1-thread-8
result: 3

在ExecutorService中还提供了invokeAny的重载方法,该方法允许执行任务的超时设置。

<T> T invokeAny(Collection<? extends Callable<T>> tasks,
            long timeout, TimeUnit unit)
            throws InterruptedException,
                    ExecutionException, TimeoutException;

2.3 invokeAll

invokeAll方法同样可用于异步处理批量的任务,但是该方法关心所有异步任务的运行,invokeAll方法同样也是阻塞方法,一直等待所有的异步任务执行结束并返回结果

3 Future的不足之处

  1. 无法被动接收异步任务的计算结果:虽然我们可以主动将异步任务提交给线程池中的线程来执行,但是待异步任务结束后,主(当前)线程无法得到任务完成与否的通知,它需要通过get方法主动获取计算结果。Google Guava的Future提供了解决方法,可参考:Google Guava的Future
  2. Future间彼此孤立:有时某一个耗时很长的异步任务执行结束以后,你还想利用它返回的结果再做进一步的运算,该运算也会是一个异步任务,两者之间的关系需要程序开发人员手动进行绑定赋予,Future并不能将其形成一个任务流(pipeline),每一个Future彼此之间都是孤立的
  3. Future没有很好的错误处理机制:截至目前,如果某个异步任务在执行的过程中发生了异常错误,调用者无法被动获知,必须通过捕获get方法的异常才能知道异步任务是否出现了错误,从而再做进一步的处理。
;