多线程 | CompletableFutureAPI简单介绍

发布于:2024-09-19 ⋅ 阅读:(14) ⋅ 点赞:(0)


本文从实例出发,介绍 CompletableFuture 基本用法。不过讲的再多,不如亲自上手练习一下。所以建议各位小伙伴看完,上机练习一把,快速掌握 CompletableFuture。

1. 案例

我们使用模拟个商品详情页接口写个demo

Future方案

一些业务场景我们需要使用多线程异步执行任务,加快任务执行速度。 Java 提供
RunnableFuture<V> 两个接口用来实现异步任务逻辑。
虽然 Future<V> 可以获取任务执行结果,但是获取方式十分不变。我们不得不使用 Future#get 阻塞调用线程,或者使用轮询方式判断 Future#isDone 任务是否结束,再获取结果。
这两种处理方式都不是很优雅,JDK8 之前并发类库没有提供相关的异步回调实现方式。没办法,我们只好借助第三方类库,如 Guava,扩展 Future,增加支持回调功能。相关代码如下:

ExecutorService executorService = Executors.newFixedThreadPool(10);
// Guava ListenableFutureTask
ListenableFutureTask<String> listenableFutureTask = ListenableFutureTask.create(() -> {
    sleep(2, TimeUnit.SECONDS) ;
    return "Future create";
});
//增加回消处理,注后这生要设置一个线程池参效哦
Futures.addCallback(listenableFutureTask, new FutureCallback<String>(){
    @Override 
    public void onSuccess(@NullableDecl String result) {
        System.out.println("处理返回结果:"+result);
    }
    @Override 
    public void onFailure(Throwable throwable){
        System.out.println("处理异常");
    }
},Executors.newSingleThreadScheduledExecutor()) ;
executorService.submit(listenableFutureTask);

虽然这种方式增强了 Java 异步编程能力,但是还是无法解决多个异步任务需要相互依赖的场景。
举一个生活上的例子,假如我们需要出去旅游,需要完成三个任务:
任务一:获取商品信息
任务二:获取促销信息
任务三:获取库存信息
很显然任务一和任务二没有相关性,可以单独执行。但是任务三必须等待任务一与任务二结束之后,才能获取库存服务。
为了使任务三时执行时能获取到任务一与任务二执行结果,我们还需要借助 CountDownLatch(下篇我们再介绍) 。

ExecutorService executorService = Executors.newFixedThreadPool( 10);
CountDownLatch countDownLatch = new CountDownLatch(2);
// 任务 1 获取商品
Future<String> productFuture = executorService.submit() -> {
    sleep(3, TimeUnit.SECONDS) ;
    System.out.println("获取商品");
    countDownLatch. countDown();
    return "商品";
});
// 任务 2 获取促销
Future<String> promotionFuture = executorService.submit(() -> {
    sleep(5, TimeUnit.SECONDS);
    System.out.println("获取促销");
    countDownLatch.countDown();
    return"促销";
});
// 使用 countDownLatch 等待任务 1 与任务2完成
countDownLatch.await();
Future<String> stockFuture = executorService. submit ( -> {
    System.out.println("根据商品里的skuId和促销信息获取库存");
    sleep(5, TimeUnit.SECONDS);
    return "库存";
});
System.out.println(orderCar.get());
/**
* 执行结果:
* 获取商品
* 商品
* 获取促销
* 促销
* 根据商品里的skuId和促销信息获取库存
* 库存
*/

CompletableFuture方案

JDK8 之后,Java 新增一个功能十分强大的类:CompletableFuture。单独使用这个类就可以轻松的完成上面的需求:

// 任务1 获取商品
CompletableFuture<String> productFuture = CompletableFuture. supplyAsync((0) -> {
    System.out.println("获取商品");
    Sleep(3, TimeUnit.SECONDS);
    return "商品";
});
//任务2 获取促销
CompletableFuture<String> promotionFuture = CompletableFuture.supplyAsync(() -> {
    sleep(5, TimeUnit.SECONDS) ;
    System.out.println("获取促销");
    return "促销";
});

//任务 3:任务 1 与任务 2 都完成后去获取库存
CompletableFutures<String> stockFuture = productFuture.thenCombine(promotionFuture,(product, promotion) ->{
    System.out.println("根据商品里的skuId和促销信息获取库存");
    Sleep(5, TimeUnit.SECONDS);
    return "库存";
});
//任务 3 执行结果
System.out.println(stockFuture.join());
/**
* 执行结果:
* 获取商品
* 商品
* 获取促销
* 促销
* 根据商品里的skuId和促销信息获取库存
* 库存
*/

对比 Future,CompletableFuture 优点在于:

  • 不需要手工分配线程,JDK 自动分配
  • 代码语义清晰,异步任务链式调用
  • 支持编排异步任务

2. CompletableFuture方法一览

在这里插入图片描述

使用new方法

CompletableFuture<Double> futurePrice = new CompletableFuture<>();使用CompletableFuture#completedFuture静态方法创建

public static <U> CompletableFuture<U> completedFuture(U value) {   
	return new CompletableFuture<U>((value == null) ? NIL : value);
}

参数的值为任务执行完的结果,一般该方法在实际应用中较少应用

supplyAsync方法

使用 CompletableFuture#supplyAsync静态方法创建 supplyAsync有两个重载方法:

//方法一
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    return asyncSupplyStage(asyncPool, supplier);
}
//方法二
public static <U>  CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) {
    return asyncSupplyStage(screenExecutor(executor), supplier);
}

runAsync方法

使用CompletableFuture#runAsync静态方法创建 runAsync有两个重载方法

//方法一
public static CompletableFuture<Void> runAsync(Runnable runnable) {
    return asyncRunStage(asyncPool, runnable);
}
//方法二
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) {
    return asyncRunStage(screenExecutor(executor), runnable);
}

两个重载方法之间的区别 => 后者可以传入自定义Executor,前者是默认的,使用的ForkJoinPool

supplyAsyncrunAsync方法之间的区别 => 前者有返回值,后者无返回值
Supplier是函数式接口,因此该方法需要传入该接口的实现类,追踪源码会发现在run方法中会调用该接口的方法。因此使用该方法创建CompletableFuture对象只需重写Supplier中的get方法,在get方法中定义任务即可。又因为函数式接口可以使用Lambda表达式,和new创建CompletableFuture对象相比代码会「简洁」不少

结果的获取:」 对于结果的获取CompltableFuture类提供了四种方式

//方式一
public T get()
//方式二
public T get(long timeout, TimeUnit unit)
//方式三
public T getNow(T valueIfAbsent)
//方式四
public T join()

说明:

  • get()和get(long timeout, TimeUnit unit) => 在Future中就已经提供了,后者提供超时处理,如果在指定时间内未获取结果将抛出超时异常
  • getNow => 立即获取结果不阻塞,结果计算已完成将返回结果或计算过程中的异常,如果未计算完成将返回设定的valueIfAbsent值
  • join => 方法里不会抛出异常
    示例:
public class AcquireResultTest {
  public static void main(String[] args) throws ExecutionException, InterruptedException {
    //getNow方法测试
    CompletableFuture<String> cp1 = CompletableFuture.supplyAsync(() -> {
      try
       {
          Thread.sleep(60 *  1000 *  60 );
       }  catch (InterruptedException e) {
          e.printStackTrace();
       }
 	   return "hello world";
 	});
	System.out.println(cp1.getNow("hello h2t"));
	//join方法测试
    CompletableFuture<Integer> cp2 = CompletableFuture.supplyAsync((()->1/0));
    System.out.println(cp2.join());
    //get方法测试
    CompletableFuture<Integer> cp3 = CompletableFuture.supplyAsync((()->1/0));
    System.out.println(cp3.get());
   }
}

说明:

  • 第一个执行结果为hello h2t,因为要先睡上1分钟结果不能立即获取
  • join方法获取结果方法里不会抛异常,但是执行结果会抛异常,抛出的异常为CompletionException
  • get方法获取结果方法里将抛出异常,执行结果抛出的异常为ExecutionException
  • 「异常处理:」使用静态方法创建的CompletableFuture对象无需显示处理异常,使用new创建的对象需要调用completeExceptionally方法设置捕获到的异常,

举例说明:

CompletableFuture completableFuture = new CompletableFuture();
new Thread(() -> {
  try{
      //doSomething,调用complete方法将其他方法的执行结果记录在completableFuture对象中
       completableFuture.complete(null);
  } catch (Exception e) {
      //异常处理
      completableFuture.completeExceptionally(e);
  }
}).start();

同步方法Pick异步方法查询所有店铺某个商品价格
店铺为一个列表:

private static List<Shop> shopList = Arrays.asList(
        new Shop("BestPrice"),
        new Shop("LetsSaveBig"),
        new Shop("MyFavoriteShop"),
        new Shop("BuyItAll")
);

同步方法:

private static List<String> findPriceSync(String product) {
    return shopList.stream()
            .map(shop -> String.format("%s price is %.2f",
                    shop.getName(), shop.getPrice(product)))  //格式转换
            .collect(Collectors.toList());
}

异步方法:

private static List<String> findPriceAsync(String product) {
    List<CompletableFuture<String>> completableFutureList = shopList.stream()
            //转异步执行
            .map(shop -> CompletableFuture.supplyAsync(
                    () -> String.format("%s price is %.2f",
                            shop.getName(), shop.getPrice(product))))  //格式转换
            .collect(Collectors.toList());

    return completableFutureList.stream()
            .map(CompletableFuture::join)  //获取结果不会抛出异常
            .collect(Collectors.toList());
}

为什么仍需要CompletableFuture

对于简单的业务场景使用Future完全可以,但是想将多个异步任务的计算结果组合起来,后一个异步任务的计算结果需要前一个异步任务的值等等,使用Future提供的那点API就囊中羞涩,处理起来不够优雅,这时候还是让CompletableFuture以「声明式」的方式优雅的处理这些需求。而且在Future编程中想要拿到Future的值然后拿这个值去做后续的计算任务,只能通过轮询的方式去判断任务是否完成这样非常占CPU并且代码也不优雅,用伪代码表示如下:

while(future.isDone()) {
    result = future.get();
    doSomrthingWithResult(result);
} 

但CompletableFuture提供了API帮助我们实现这样的需求

其他API介绍

whenComplete

计算结果的处理:对前面计算结果进行处理,无法返回新值
提供了三个方法:

//方法一
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
//方法二
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
//方法三
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)

说明:

  • BiFunction<? super T,? super U,? extends V> fn参数 => 定义对结果的处理
  • Executor executor参数 => 自定义线程池
  • 以async结尾的方法将会在一个新的线程中执行组合操作
    示例:
public class WhenCompleteTest {
    public static void main(String[] args) {
        CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "hello");
        CompletableFuture<String> cf2 = cf1.whenComplete((v, e) ->
                System.out.println(String.format("value:%s, exception:%s", v, e)));
        System.out.println(cf2.join());
    }
}

thenApply

将前面计算结果的的CompletableFuture传递给thenApply,返回thenApply处理后的结果。可以认为通过thenApply方法实现CompletableFuture至CompletableFuture的转换。白话一点就是将CompletableFuture的计算结果作为thenApply方法的参数,返回thenApply方法处理后的结果
提供了三个方法


//方法一
public <U> CompletableFuture<U> thenApply(
    Function<? super T,? extends U> fn) {
    return uniApplyStage(null, fn);
}

//方法二
public <U> CompletableFuture<U> thenApplyAsync(
    Function<? super T,? extends U> fn) {
    return uniApplyStage(asyncPool, fn);
}

//方法三
public <U> CompletableFuture<U> thenApplyAsync(
    Function<? super T,? extends U> fn, Executor executor) {
    return uniApplyStage(screenExecutor(executor), fn);
}

说明:

  • Function<? super T,? extends U> fn参数 => 对前一个CompletableFuture 计算结果的转化操作
  • Executor executor参数 => 自定义线程池
  • 以async结尾的方法将会在一个新的线程中执行组合操作 示例:
public class ThenApplyTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(ThenApplyTest::randomInteger).thenApply((i) -> i * 8);
        System.out.println(result.get());
    }

    public static Integer randomInteger() {
        return 10;
    }
}

这里将前一个CompletableFuture计算出来的结果扩大八倍

thenAccept

thenApply也可以归类为对结果的处理,thenAccept和thenApply的区别就是没有返回值
提供了三个方法:

//方法一
public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
    return uniAcceptStage(null, action);
}

//方法二
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) {
    return uniAcceptStage(asyncPool, action);
}

//方法三
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action,
                                               Executor executor) {
    return uniAcceptStage(screenExecutor(executor), action);
}

说明:

  • Consumer<? super T> action参数 => 对前一个CompletableFuture计算结果的操作
  • Executor executor参数 => 自定义线程池
  • 同理以async结尾的方法将会在一个新的线程中执行组合操作 示例:
public class ThenAcceptTest {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(ThenAcceptTest::getList).thenAccept(strList -> strList.stream()
                .forEach(m -> System.out.println(m)));
    }

    public static List<String> getList() {
        return Arrays.asList("a", "b", "c");
    }
}

将前一个CompletableFuture计算出来的结果打印出来

thenCompose

异步结果流水化,可以将两个异步操作进行流水操作
提供了三个方法:

//方法一
public <U> CompletableFuture<U> thenCompose(
    Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(null, fn);
}

//方法二
public <U> CompletableFuture<U> thenComposeAsync(
    Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(asyncPool, fn);
}

//方法三
public <U> CompletableFuture<U> thenComposeAsync(
    Function<? super T, ? extends CompletionStage<U>> fn,
    Executor executor) {
    return uniComposeStage(screenExecutor(executor), fn);
}

说明:

  • Function<? super T, ? extends CompletionStage<?>> fn参数 => 当前CompletableFuture计算结果的执行
  • Executor executor参数 => 自定义线程池
  • 同理以async结尾的方法将会在一个新的线程中执行组合操作
    示例:
public class ThenComposeTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(ThenComposeTest::getInteger)
                .thenCompose(i -> CompletableFuture.supplyAsync(() -> i * 10));
        System.out.println(result.get());
    }

    private static int getInteger() {
        return 666;
    }

    private static int expandValue(int num) {
        return num * 10;
    }
}

thenCombine

thenCombine方法将两个无关的CompletableFuture组合起来,第二个Completable并不依赖第一个Completable的结果
提供了三个方法:

//方法一
public <U,V> CompletableFuture<V> thenCombine( 
    CompletionStage<? extends U> other,
    BiFunction<? super T,? super U,? extends V> fn) {
    return biApplyStage(null, other, fn);
}
  //方法二
  public <U,V> CompletableFuture<V> thenCombineAsync(
      CompletionStage<? extends U> other,
      BiFunction<? super T,? super U,? extends V> fn) {
      return biApplyStage(asyncPool, other, fn);
  }

  //方法三
  public <U,V> CompletableFuture<V> thenCombineAsync(
      CompletionStage<? extends U> other,
      BiFunction<? super T,? super U,? extends V> fn, Executor executor) {
      return biApplyStage(screenExecutor(executor), other, fn);
  }

说明:

  • CompletionStage<? extends U> other参数 => 新的CompletableFuture的计算结果
  • BiFunction<? super T,? super U,? extends V> fn参数 => 定义了两个CompletableFuture对象「完成计算后」如何合并结果,该参数是一个函数式接口,因此可以使用Lambda表达式
  • Executor executor参数 => 自定义线程池
  • 同理以async结尾的方法将会在一个新的线程中执行组合操作

示例:

public class ThenCombineTest {
    private static Random random = new Random();
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(ThenCombineTest::randomInteger).thenCombine(
                CompletableFuture.supplyAsync(ThenCombineTest::randomInteger), (i, j) -> i * j
        );

        System.out.println(result.get());
    }

    public static Integer randomInteger() {
        return random.nextInt(100);
    }
}

allOf&anyOf

//allOf
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
    return andTree(cfs, 0, cfs.length - 1);
}
//anyOf
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {
    return orTree(cfs, 0, cfs.length - 1);
}

说明:

  • allOf => 所有的CompletableFuture都执行完后执行计算。
  • anyOf => 任意一个CompletableFuture执行完后就会执行计算

示例:allOf方法测试

public class AllOfTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Void> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("hello");
            return null;
        });
        CompletableFuture<Void> future2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("world"); return null;
        });
        CompletableFuture<Void> result = CompletableFuture.allOf(future1, future2);
        System.out.println(result.get());
    }
}

allOf方法没有返回值,适合没有返回值并且需要前面所有任务执行完毕才能执行后续任务的应用场景

public class AnyOfTest {
    private static Random random = new Random();
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            randomSleep();
            System.out.println("hello");
            return "hello";});
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            randomSleep();
            System.out.println("world");
            return "world";
        });
        CompletableFuture<Object> result = CompletableFuture.anyOf(future1, future2);
        System.out.println(result.get());
   }

    private static void randomSleep() {
        try {
            Thread.sleep(random.nextInt(10));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

两个线程都会将结果打印出来,但是get方法只会返回最先完成任务的结果。该方法比较适合只要有一个返回值就可以继续执行其他任务的应用场景

supplyAsync

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    return asyncSupplyStage(asyncPool, supplier);
}
static <U> CompletableFuture<U> asyncSupplyStage(Executor e, Supplier<U> f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture<U> d = new CompletableFuture<U>();
    e.execute(new AsyncSupply<U>(d, f));
    return d;
}

底层调用的是线程池去执行任务,而CompletableFuture中默认线程池为ForkJoinPool

private static final Executor asyncPool = useCommonPool ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

ForkJoinPool线程池的大小取决于CPU的核数。CPU密集型任务线程池大小配置为CPU核心数就可以了,但是IO密集型,线程池的大小由
CPU数量 * CPU利用率 * (1 + 线程等待时间/线程CPU时间)
确定。而CompletableFuture的应用场景就是IO密集型任务,因此默认的ForkJoinPool一般无法达到最佳性能,我们需自己根据业务创建线程池。

3. 注意点

很多方法都提供了异步实现【带async后缀】,但是需小心谨慎使用这些异步方法,因为异步意味着存在上下文切换,可能性能不一定比同步好。如果需要使用异步的方法,「先做测试」,用测试数据说话!!!

4. 总结

本次只是介绍CompletableFuture的常用方法介绍,具体还需要结合根据自己业务代码进行使用。


网站公告

今日签到

点亮在社区的每一天
去签到