《Java并发编程实战》课程学习笔记(十六)

Future 和 CompletableFuture

Future

  • 利用 Java 并发包提供的 Future 可以很容易获得异步任务的执行结果,无论异步任务是通过线程池 ThreadPoolExecutor 执行的,还是通过手工创建子线程来执行的。
  • 利用多线程可以快速将一些串行的任务并行化,从而提高性能;如果任务之间有依赖关系,比如当前任务依赖前一个任务的执行结果,这种问题基本上都可以用 Future 来解决。

如何获取任务执行结果

  • ThreadPoolExecutor 的 void execute(Runnable command) 方法虽然可以提交任务,但是却没有办法获取任务的执行结果(execute() 方法没有返回值)。而很多场景下,我们又都是需要获取任务的执行结果的。
  • Java 通过 ThreadPoolExecutor 提供的 3 个 submit() 方法和 1 个 FutureTask 工具类来支持获得任务执行结果的需求。
    // 提交 Runnable 任务
    Future<?> submit(Runnable task);
    // 提交 Callable 任务
    <T> Future<T> submit(Callable<T> task);
    // 提交 Runnable 任务及结果引⽤
    <T> Future<T> submit(Runnable task, T result);
    
    • 它们的返回值都是 Future 接口,Future 接口有 5 个方法,它们分别是取消任务的方法 cancel()、判断任务是否已取消的方法 isCancelled()、判断任务是否已结束的方法 isDone() 以及 2 个获得任务执行结果的 get() 和 get(timeout, unit),其中最后一个 get(timeout, unit) 支持超时机制。
      // 取消任务
      boolean cancel(boolean mayInterruptIfRunning);
      // 判断任务是否已取消
      boolean isCancelled();
      // 判断任务是否已结束
      boolean isDone();
      // 获得任务执⾏结果
      get();
      // 获得任务执⾏结果,⽀持超时
      get(long timeout, TimeUnit unit);
      
    • 我们提交的任务不但能够获取任务执行结果,还可以取消任务。
    • 不过需要注意的是:这两个 get() 方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用 get() 方法的线程会阻塞,直到任务执行完才会被唤醒。
    • 这 3 个 submit() 方法之间的区别在于方法参数不同:
      • 提交 Runnable 任务 submit(Runnable task)
        • 这个方法的参数是一个 Runnable 接口,Runnable 接口的 run() 方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的 Future 仅可以用来断言任务已经结束了,类似于 Thread.join()。
      • 提交 Callable 任务 submit(Callable<T> task)
        • 这个方法的参数是一个 Callable 接口,它只有一个 call() 方法,并且这个方法是有返回值的,所以这个方法返回的 Future 对象可以通过调用其 get() 方法来获取任务的执行结果。
      • 提交 Runnable 任务及结果引用 submit(Runnable task, T result)
        • 假设这个方法返回的 Future 对象是 f,f.get() 的返回值就是传给 submit() 方法的参数 result。
        • result 相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据。

FutureTask 工具类

  • FutureTask 是一个实实在在的工具类,这个工具类有两个构造函数,它们的参数和 submit() 方法类似:
    FutureTask(Callable<V> callable);
    FutureTask(Runnable runnable, V result);
    
    • FutureTask 实现了 Runnable 和 Future 接口,由于实现了 Runnable 接口,所以可以将 FutureTask 对象作为任务提交给 ThreadPoolExecutor 去执行,也可以直接被 Thread 执行;
    • 又因为实现了 Future 接口,所以也能用来获得任务的执行结果。
      // 创建 FutureTask
      FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);
      // 创建线程池
      ExecutorService es = Executors.newCachedThreadPool();
      // 提交 FutureTask
      es.submit(futureTask);
      // 获取计算结果
      Integer result = futureTask.get();
      
    • FutureTask 对象直接被 Thread 执行的示例代码,利用 FutureTask 对象可以很容易获取子线程的执行结果:
      // 创建 FutureTask
      FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);
      // 创建并启动线程
      Thread T1 = new Thread(futureTask);
      T1.start();
      // 获取计算结果
      Integer result = futureTask.get();
      

CompletableFuture

  • 创建 CompletableFuture 对象主要靠 4 个静态方法:
    // 使⽤默认线程池
    static CompletableFuture<Void> runAsync(Runnable runnable)
    static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
    // 可以指定线程池
    static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
    static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
    
    • runAsync(Runnable runnable) 和 supplyAsync(Supplier supplier)
      • 它们之间的区别是:Runnable 接口的 run() 方法没有返回值,而 Supplier 接口的 get() 方法是有返回值的。
    • 前两个方法和后两个方法的区别在于:后两个方法可以指定线程池参数。
    • 默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数。
  • 如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议要根据不同的业务类型创建不同的线程池,以避免互相干扰。

猜你喜欢

转载自blog.csdn.net/fangzhan666/article/details/131065997