Future

作者: 我可能是个假开发 | 来源:发表于2023-03-02 20:42 被阅读0次

    一、获取任务执行结果

    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 个方法:

    // 取消任务
    boolean cancel(boolean mayInterruptIfRunning);
    // 判断任务是否已取消  
    boolean isCancelled();
    // 判断任务是否已结束
    boolean isDone();
    // 获得任务执行结果
    get();
    // 获得任务执行结果,支持超时
    get(long timeout, TimeUnit unit);
    
    • 取消任务的方法 cancel()
    • 判断任务是否已取消的方法 isCancelled()
    • 判断任务是否已结束的方法 isDone()
    • 获得任务执行结果的 get() 和 get(timeout, unit)

    get(timeout, unit) 支持超时机制。提交的任务不但能够获取任务执行结果,还可以取消任务。不过需要注意的是:这两个 get() 方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用 get() 方法的线程会阻塞,直到任务执行完才会被唤醒。

    submit() 方法之间的区别在于方法参数不同:

    • 提交 Runnable 任务 submit(Runnable task) :这个方法的参数是一个 Runnable 接口,Runnable 接口的 run() 方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的 Future 仅可以用来断言任务已经结束了,类似于 Thread.join()。
    • 提交 Callable 任务 submit(Callable task):这个方法的参数是一个 Callable 接口,它只有一个 call() 方法,并且这个方法是有返回值的,所以这个方法返回的 Future 对象可以通过调用其 get() 方法来获取任务的执行结果。
    • 提交 Runnable 任务及结果引用 submit(Runnable task, T result):假设这个方法返回的 Future 对象是 f,f.get() 的返回值就是传给 submit() 方法的参数 result。
    ExecutorService executor = Executors.newFixedThreadPool(1);
    // 创建Result对象r
    Result r = new Result();
    r.setAAA(a);
    // 提交任务
    Future<Result> future = executor.submit(new Task(r), r);  
    Result fr = future.get();
    // 下面等式成立
    fr === r;
    fr.getAAA() === a;
    fr.getXXX() === x
    
    class Task implements Runnable{
      Result r;
      //通过构造函数传入result
      Task(Result r){
        this.r = r;
      }
      void run() {
        //可以操作result
        a = r.getAAA();
        r.setXXX(x);
      }
    }
    

    Runnable 接口的实现类 Task 声明了一个有参构造函数 Task(Result r) ,创建 Task 对象的时候传入了 result 对象,这样就能在类 Task 的 run() 方法中对 result 进行各种操作了。result 相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据。

    二、FutureTask

    Future 是一个接口,而 FutureTask 是一个实实在在的工具类,这个工具类有两个构造函数,它们的参数和前面介绍的 submit() 方法类似。

    FutureTask(Callable<V> callable);
    FutureTask(Runnable runnable, V result);
    

    FutureTask 实现了 Runnable 和 Future 接口,由于实现了 Runnable 接口,所以可以将 FutureTask 对象作为任务提交给 ThreadPoolExecutor 去执行,也可以直接被 Thread 执行;又因为实现了 Future 接口,所以也能用来获得任务的执行结果。

    1.将 FutureTask 对象提交给 ThreadPoolExecutor 去执行:

    // 创建FutureTask
    FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);
    // 创建线程池
    ExecutorService es = Executors.newCachedThreadPool();
    // 提交FutureTask 
    es.submit(futureTask);
    // 获取计算结果
    Integer result = futureTask.get();
    

    2.FutureTask 对象直接被 Thread 执行

    利用 FutureTask 对象可以很容易获取子线程的执行结果:

    // 创建FutureTask
    FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);
    // 创建并启动线程
    Thread T1 = new Thread(futureTask);
    T1.start();
    // 获取计算结果
    Integer result = futureTask.get();
    

    三、实现最优的“烧水泡茶”程序

    烧水泡茶最优的工序:


    烧水泡茶.png

    并发编程可以总结为三个核心问题:

    • 分工
    • 同步
    • 互斥

    对于烧水泡茶这个程序,一种最优的分工方案:
    用两个线程 T1 和 T2 来完成烧水泡茶程序,T1 负责洗水壶、烧开水、泡茶这三道工序,T2 负责洗茶壶、洗茶杯、拿茶叶三道工序,其中 T1 在执行泡茶这道工序时需要等待 T2 完成拿茶叶的工序。对于 T1 的这个等待动作,可以使用Thread.join()、CountDownLatch,甚至阻塞队列都可以解决,此处用 Future 特性来实现。

    // 创建任务T2的FutureTask
    FutureTask<String> ft2 = new FutureTask<>(new T2Task());
    // 创建任务T1的FutureTask
    FutureTask<String> ft1 = new FutureTask<>(new T1Task(ft2));
    // 线程T1执行任务ft1
    Thread T1 = new Thread(ft1);
    T1.start();
    // 线程T2执行任务ft2
    Thread T2 = new Thread(ft2);
    T2.start();
    // 等待线程T1执行结果
    System.out.println(ft1.get());
    
    // T1Task需要执行的任务:
    // 洗水壶、烧开水、泡茶
    class T1Task implements Callable<String>{
      FutureTask<String> ft2;
      // T1任务需要T2任务的FutureTask
      T1Task(FutureTask<String> ft2){
        this.ft2 = ft2;
      }
      @Override
      String call() throws Exception {
        System.out.println("T1:洗水壶...");
        TimeUnit.SECONDS.sleep(1);
        
        System.out.println("T1:烧开水...");
        TimeUnit.SECONDS.sleep(15);
        // 获取T2线程的茶叶  
        String tf = ft2.get();
        System.out.println("T1:拿到茶叶:"+tf);
    
        System.out.println("T1:泡茶...");
        return "上茶:" + tf;
      }
    }
    // T2Task需要执行的任务:
    // 洗茶壶、洗茶杯、拿茶叶
    class T2Task implements Callable<String> {
      @Override
      String call() throws Exception {
        System.out.println("T2:洗茶壶...");
        TimeUnit.SECONDS.sleep(1);
    
        System.out.println("T2:洗茶杯...");
        TimeUnit.SECONDS.sleep(2);
    
        System.out.println("T2:拿茶叶...");
        TimeUnit.SECONDS.sleep(1);
        return "龙井";
      }
    }
    // 一次执行结果:
    T1:洗水壶...
    T2:洗茶壶...
    T1:烧开水...
    T2:洗茶杯...
    T2:拿茶叶...
    T1:拿到茶叶:龙井
    T1:泡茶...
    上茶:龙井
    

    首先,创建了两个 FutureTask——ft1 和 ft2,ft1 完成洗水壶、烧开水、泡茶的任务,ft2 完成洗茶壶、洗茶杯、拿茶叶的任务;这里需要注意的是 ft1 这个任务在执行泡茶任务前,需要等待 ft2 把茶叶拿来,所以 ft1 内部需要引用 ft2,并在执行泡茶之前,调用 ft2 的 get() 方法实现等待。

    相关文章

      网友评论

          本文标题:Future

          本文链接:https://www.haomeiwen.com/subject/fmajldtx.html