Java8新特性 之CompletableFuture

Java8异步类CompletableFuture详解 JDK8引入了一个新的类CompletableFuture 阅读详情

面试

CompletableFuture使用了空间换时间的思路,异步查询也可以使用FutureTask。
但是使用Future有个问题,就是在于返回获取异步结果的时候需要有等待状态,这个等待的状态是需要消耗时间进行堵塞的。
CompletableFuture只有在最后汇总的时候才进行堵塞,这时所有任务都已经完成了。

通过线程池性能稳定,也可以获取执行结果,并捕获异常。但是,在业务复杂的情况下,一个异步调用可能会依赖于另一个异步调用的执行结果。

CompletableFuture引入

Future是JDK5添加的类,用来描述一个异步计算的结果。可以使用isDone方法检查计算是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,也可以使用cancel方法停止任务的执行。

public static void main(String[] args) throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    Future<Integer> f = executorService.submit(() ->{
        // 长时间的异步计算
        // 然后返回结果
        System.out.println("A");
        return 100;
    });
//        while(!f.isDone()){
//            System.out.println("D");
//        }
    System.out.println("B");
    Integer i = f.get();
    System.out.println("C" + i);
    executorService.shutdown();
    return;
}

结果:
B
A
C100

虽然Future以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞或者轮询的方式得到任务的结果。阻塞的方式显然和异步编程的初衷相违背,轮询的方式又会耗费无谓的CPU资源,而且也不能及时地得到计算结果,所以可以用观察者设计模式,当计算结果完成及时通知监听者。

在JDK8里面,就新增了CompletableFuture类,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。

CompletableFuture实现了Future、CompletionStage接口。CompletionStage接口定义了可与其他Future组合成异步计算契约。

使用CompletableFuture作为Future实现

public class CompletableFuture<T> implements Future<T>, CompletionStage<T>

CompletableFuture类实现Future接口,因此可以将其用作Future实现,但需要额外的完成实现逻辑。

例如:可以使用无构参构造函数创建此类的实例,然后使用complete方法完成。消费者可以使用get方法来阻塞当前线程,直到get()结果。
在下面的示例中,有一个创建CompletableFuture实例的方法,然后在另一个线程中计算并立即返回Future。
计算完成后,该方法通过将结果提供给完整方法来完成Future:

public static void main(String[] args) throws Exception{
    Future<String> future = calculateAsync();
    System.out.println(Thread.currentThread().getName() + " main");
    System.out.println(future.get());
}

public static Future<String> calculateAsync() {
    CompletableFuture<String> completableFuture = new CompletableFuture<>();
    Executors.newCachedThreadPool().execute(() -> {
        System.out.println(Thread.currentThread().getName() + " execute");
        String result = "Hello " + " World";
        completableFuture.complete(result);
    });
    return completableFuture;
}

结果:
main main
pool-1-thread-1 execute
Hello  World

Async后缀方法

public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

CompletableFuture类中的API的大多数方法都有两个带有Async后缀的附加修饰。这些方法表示用于异步线程。

  • 没有Async后缀的方法使用调用线程运行下一个执行线程阶段。
  • 带有Async后缀的方法默认使用ForkJoinPool.commonPool()线程池的fork/join实现运算任务。
  • 带有Async后缀的方法并且传递Executor对象,由Executor任务去运行。

什么是ForkJoinPool

ForkJoinPool是ExecutorSerice的一个补充,而不是替代品。
JAVA8中CompeleteFuture、并发流等都是基于ForkJoinPool实现,默认并发数是CPU核数。

创建一个异步任务

CompletableFuture.completedFuture是一个静态辅助方法,用来返回一个已经计算好的CompletableFuture。

public static <U> CompletableFuture<U> completedFuture(U value)

Async结尾的方法都是可以异步执行的,如果指定了线程池,会在指定的线程池中执行,如果没有指定,默认会在ForkJoinPool.commonPool()中执行。

  • runAsync方法:它以Runnabel函数式接口类型为参数,所以CompletableFuture的计算结果为空。
  • supplyAsync方法:以Supplier<U>函数式接口类型为参数,所以CompletableFuture有返回值,且计算结果类型为U。
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> System.out.println("runAsync"));
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "supplyAsync");

System.out.println(future1.get());
System.out.println(future2.get());

结果:
runAsync
null
supplyAsync

计算完成时对结果的处理 whenComplete、exceptionally、handle

当CompletableFuture的计算结果完成,或者抛出异常的时候,我们可以执行特定的Action。

  • 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)

参数类型为 BiConsumer<? super T, ? super Throwable> 会获取上一步计算的计算结果和异常信息。
以Async结尾的方法可能会使用其它的线程去执行,如果使用相同的线程池,也可能会被同一个线程选中执行。

public class ThreadUtil {
    public static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }
}

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    ThreadUtil.sleep(100);
    return 20;
}).whenCompleteAsync((v, e) -> {
    System.out.println("VV:" + v);
    System.out.println("EE:" + e);
});
System.out.println("A");
System.out.println(future.get());
System.out.println("B");

结果:
A
VV:20
EE:null
20
B
  • public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)

该方法是对异常情况的处理,当函数异常时执行。

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    ThreadUtil.sleep(100);
    return 20/0;
}).whenCompleteAsync((v, e) -> {
    System.out.println("VV:" + v);
    System.out.println("EE:" + e);
}).exceptionally((e)->{
    System.out.println(e.getMessage());
    return 30;
});
System.out.println("A");
System.out.println(future.get());
System.out.println("B");

结果:
A
VV:null
EE:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
30
B
  • public <U> CompletableFuture<U> handle(BiFunction<? super T,Throwable,? extends U> fn)
  • public <U> CompletableFuture<U> handleAsync(BiFunction<? super T,Throwable,? extends U> fn)
  • public <U> CompletableFuture<U> handleAsync(BiFunction<? super T,Throwable,? extends U> fn, Executor executor)

handle 方法和whenComplete方法类似,只不过接收的是一个 BiFunction<? super T,Throwable,? extends U> fn 类型的参数,因此有 whenComplete 方法和 转换的功能 (thenApply)。
以Async结尾的方法可能会使用其它的线程去执行,如果使用相同的线程池,也可能会被同一个线程选中执行。

CompletableFuture<Integer> future = CompletableFuture
        .supplyAsync(() -> {
            ThreadUtil.sleep(100);
            return 20;
        })
        .handle((t, e)->{
            System.out.println("TT:" + t);
            System.out.println("EE:" + e);
            return 10;
        });
System.out.println("A");
System.out.println(future.get());
System.out.println("B");
结果:
A
TT:20
EE:null
10
B
// 异常情况
CompletableFuture<Integer> future = CompletableFuture
        .supplyAsync(() -> {
            ThreadUtil.sleep(100);
            return 20/0;
        })
        .handle((t, e)->{
            System.out.println("TT:" + t);
            System.out.println("EE:" + e);
            return 10;
        });
System.out.println("A");
System.out.println(future.get());
System.out.println("B");
结果:
A
TT:null
EE:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
10
B

结果处理转换 thenApply

当前阶段正常完成以后执行,而且当前阶段的执行的结果会作为下一阶段的输入参数。
thenApplyAsync默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。
thenApply相当于回调函数(callback)。

CompletableFuture 由于有回调,可以不必等待一个计算完成而阻塞着调用线程,可以在一个结果计算完成之后紧接着执行某个Action。我们可以将这些操作串联起来。

  • public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
  • public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
  • public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
CompletableFuture<Integer> future = CompletableFuture
        .supplyAsync(() -> {
            ThreadUtil.sleep(100);
            return 20;
        })
        .thenApply((f1)->{
            System.out.println("F1:" + f1);
            return f1 * 2;
        })
        .thenApply((f2)->{
            System.out.println("F2:" + f2);
            return f2 * 2;
        })
        .thenApply((f3)->{
            System.out.println("F3:" + f3);
            return f3 * 2;
        });
System.out.println("A");
System.out.println("future:" + future.get());
System.out.println("B");

结果:
A
F1:20
F2:40
F3:80
future:160
B

这些方法不是马上执行的,也不会阻塞,而是前一个执行完成后继续执行下一个。
和 handle 方法的区别是,handle 会处理正常计算值和异常,不会抛出异常。而 thenApply 只会处理正常计算值,有异常则抛出。

纯消费 thenAccept、thenRun、thenAcceptBoth、runAfterBoth

thenAccept和thenRun都是无返回值的。如果说thenApply是不停的输入输出的进行生产,那么thenAccept和thenRun就是在进行消耗。它们是整个计算的最后两个阶段。

  • thenAccept接收上一阶段的输出作为本阶段的输入
  • thenRun根本不关心前一阶段的输出,根本不不关心前一阶段的计算结果,因为它不需要输入参数

单纯的去消费结果而不会返回新的值,因些计算结果为 Void。

  • public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
  • public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
  • public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
    ThreadUtil.sleep(100);
    return 20;
}).thenAccept((c)->{
    System.out.println("CC:" + c);
}).thenAcceptAsync((c2)->{
    System.out.println("C2:" + c2);
});
System.out.println("A");
System.out.println(future.get());
System.out.println("B");

结果:
A
CC:20
C2:null
null
B
  • public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action)
  • public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action)
  • public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action, Executor executor)
    和 thenAccept 相比,参数类型多了一个 CompletionStage<? extends U> other,BiConsumer第一个参数接收第一个supplyAsync返回值,第二个参数接收CompletionStage返回值。
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName());
    ThreadUtil.sleep(100);
    return 20;
}).thenAcceptBoth(CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName());
    return 2;
}), (a, b)->{
    System.out.println(Thread.currentThread().getName() + "-AA:" + a);
    System.out.println(Thread.currentThread().getName() + "-BB:" + b);
});
System.out.println("A");
System.out.println(future.get());
System.out.println("B");

结果:
ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-2
A
ForkJoinPool.commonPool-worker-1-AA:20
ForkJoinPool.commonPool-worker-1-BB:2
null
B
  • public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other, Runnable action)
  • public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action)
  • public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor)
    runAfterBoth 和以上方法不同,传一个 Runnable 类型的参数,不接收上一级的返回值
    更彻底的:
  • public CompletableFuture<Void> thenRun(Runnable action)
  • public CompletableFuture<Void> thenRunAsync(Runnable action)
  • public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)

以上是彻底的纯消费,完全忽略计算结果。

组合 thenCompose、thenCombine

  • public <U> CompletableFuture<U> thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
  • public <U> CompletableFuture<U> thenComposeAsync(Function<? super T,? extends CompletionStage<U>> fn)
  • public <U> CompletableFuture<U> thenComposeAsync(Function<? super T,? extends CompletionStage<U>> fn, Executor executor)

以上接收类型为 Function<? super T,? extends CompletionStage<U>> fn ,fn 接收上一级返回的结果,并返回一个新的 CompletableFuture。

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName() + " A");
    ThreadUtil.sleep(100);
    return 20;
}).thenApply((f)->{
    System.out.println(Thread.currentThread().getName() + " B");
    return f + 10;
}).thenCompose((s)->{
    System.out.println(Thread.currentThread().getName() + " SS:" + s);
    return CompletableFuture.supplyAsync(() -> {
        System.out.println(Thread.currentThread().getName() + " C");
        return s * 5;
    });
});
System.out.println("A");
System.out.println(future.get());
System.out.println("B");

结果:
ForkJoinPool.commonPool-worker-1 A
A
ForkJoinPool.commonPool-worker-1 B
ForkJoinPool.commonPool-worker-1 SS:30
ForkJoinPool.commonPool-worker-2 C
150
B
  • public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
  • public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
  • public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor)

两个CompletionStage是并行执行的,它们之间并没有先后依赖顺序,other并不会等待先前的CompletableFuture执行完毕后再执行。

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    ThreadUtil.sleep(1000);
    System.out.println(Thread.currentThread().getName());
    return 20;
}).thenApply((f)->{
    ThreadUtil.sleep(1000);
    System.out.println(Thread.currentThread().getName());
    return f + 10;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName() + " thenCombineAsync");
    return 10;
}), (x, y)->{
    System.out.println(Thread.currentThread().getName() + " XX:" + x);
    System.out.println(Thread.currentThread().getName() + " YY:" + y);
    return x + y;
});
System.out.println("A");
System.out.println(future.get());
System.out.println("B");

结果:
ForkJoinPool.commonPool-worker-2 thenCombineAsync
A
ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-1 XX:30
ForkJoinPool.commonPool-worker-1 YY:10
40
B

thenCombine 和 supplyAsync 不一定哪个先哪个后,是并行执行的。

任意一个方法执行完成就结束acceptEither、applyToEither

  • public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
  • public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)
  • public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor)
Random random = new Random();
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
    ThreadUtil.sleep(random.nextInt(1000));
    return "A";
}).acceptEither(CompletableFuture.supplyAsync(() -> {
    ThreadUtil.sleep(random.nextInt(1000));
    return "B";
}), (c) -> {
    System.out.println(c);
});
System.out.println(future.get());

结果:
B
null
或者:
A
null

以上代码有时输出A,有时输出B,哪个Future先执行完就会根据它的结果计算。
acceptEither方法是当任意一个 CompletionStage 完成的时候,action 这个消费者就会被执行。这个方法返回 CompletableFuture<Void>。

  • public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T,U> fn)
  • public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T,U> fn)
  • public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T,U> fn, Executor executor)

applyToEither 方法是当任意一个 CompletionStage 完成的时候,fn会被执行,它的返回值会当作新的CompletableFuture<U>的计算结果。

Random random = new Random();
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    ThreadUtil.sleep(random.nextInt(1000));
    return "A";
}).applyToEither(CompletableFuture.supplyAsync(() -> {
    ThreadUtil.sleep(random.nextInt(1000));
    return "B";
}), (c) -> {
    System.out.println(c);
    return c + "DD";
});
System.out.println(future.get());

结果:
B
BDD
或者:
A
ADD

acceptEither 和 applyToEither 区别是: acceptEither 没有返回值,applyToEither 有返回值。

辅助方法allOf、anyOf

  • public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
    当所有的CompletableFuture都执行完后才往下执行,没有返回值。
  • public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
    当任意一个CompletableFuture执行完后就会执行计算,计算的结果相同,返回一个Object类型的值。

使用CompletableFuture提升程序性能

如果每个操作都很简单的话,没有必要用这种多线程异步的方式,因为创建线程还需要时间,还不如直接同步执行来得快。

只有当每个操作很复杂需要花费相对很长的时间(比如,调用多个其它的系统的接口;比如,商品详情页面这种需要从多个系统中查数据显示的)的时候用CompletableFuture才合适,不然区别真的不大,还不如顺序同步执行。

定义一个对象:

@Data
public class UserInfo {
    private Integer id;
    private String name;
    private Integer jobId;
    private String jobDes;
    private Integer carId;
    private String carDes;
    private Integer homeId;
    private String homeDes;
}

这个对象里面的homeid,jobid,carid都是用于匹配对应的住房信息描述,职业信息描述,购车信息描述。
对于将id转换为描述信息的方式需要通过额外的sql查询,这里做了个简单的工具类来进行模拟:

import java.util.concurrent.TimeUnit;

public class QueryUtils {

    public String queryCar(Integer carId){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "car_desc";
    }
    public String queryJob(Integer jobId){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "job_desc";
    }
    public String queryHome(Integer homeId){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "home_desc";
    }
}

假设每次查询需要消耗1s,那么遍历的一个size为n的集合查询消耗的时间就是n * 3s。

下面来使用CompletableFuture提升性能:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public class QueryUserService {

    private Supplier<QueryUtils> queryUtilsSupplier = new Supplier<QueryUtils>() {
        @Override
        public QueryUtils get() {
            return new QueryUtils();
        }
    };

    // 传统方式
    public UserInfo converUserInfo(UserInfo userInfo) {
        userInfo.setCarDes(queryUtilsSupplier.get().queryCar(userInfo.getCarId()));
        userInfo.setHomeDes(queryUtilsSupplier.get().queryHome(userInfo.getHomeId()));
        userInfo.setJobDes(queryUtilsSupplier.get().queryJob(userInfo.getJobId()));
        return userInfo;
    }

    // CompletableFuture方式
    public UserInfo converUserInfoForFuture(UserInfo userInfo) {
        CompletableFuture<String> getCarDesc = CompletableFuture.supplyAsync(() -> queryUtilsSupplier.get().queryCar(userInfo.getCarId()));
        getCarDesc.thenAccept((carDesc) -> userInfo.setCarDes(carDesc));

        CompletableFuture<String> getHomeDesc = CompletableFuture.supplyAsync(() -> queryUtilsSupplier.get().queryHome(userInfo.getHomeId()));
        getHomeDesc.thenAccept((homeDesc) -> userInfo.setHomeDes(homeDesc));

        CompletableFuture<String> getJobDesc = CompletableFuture.supplyAsync(() -> queryUtilsSupplier.get().queryJob(userInfo.getJobId()));
        getJobDesc.thenAccept((jobDesc) -> userInfo.setJobDes(jobDesc));

        CompletableFuture<Void> getUserInfo = CompletableFuture.allOf(getCarDesc, getHomeDesc, getJobDesc);
        getUserInfo.thenAccept(new Consumer<Void>() {
            @Override
            public void accept(Void result) {
                System.out.println("全部完成查询");
            }
        });
        getUserInfo.join();
        return userInfo;
    }
    public static void main(String[] args) {
        long begin = System.currentTimeMillis();
        // 多线程环境需要注意线程安全问题
        List<UserInfo> userInfoList = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i <= 20; i++) {
            UserInfo userInfo = new UserInfo();
            userInfo.setId(i);
            userInfo.setName("username" + i);
            userInfo.setCarId(i);
            userInfo.setJobId(i);
            userInfo.setHomeId(i);
            userInfoList.add(userInfo);
        }
        QueryUserService queryUserService = new QueryUserService();
        //stream 查询一个用户花费3s  并行计算后一个用户1秒左右 查询21个用户花费21秒
        userInfoList.stream().map(userInfo -> {
            userInfo = queryUserService.converUserInfoForFuture(userInfo);
            return userInfo;
        }).collect(Collectors.toList());
        System.out.println("=============");
        long end = System.currentTimeMillis();
        System.out.println(end - begin);
    }
}

CompletableFuture使用详解

static方法说明

CompletableFuture的几个 static 方法,它们可以实例化一个 CompletableFuture 实例。

public static <U> CompletableFuture<U> completedFuture(U value)
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
  • completedFuture 返回一个已经计算好的CompletableFuture。
  • runAsync 方法接收的是 Runnable 的实例,但是它没有返回值。
  • supplyAsync 方法是JDK8函数式接口,无参数,会返回一个结果。
  • 这两个方法是 executor 的升级,表示让任务在指定的线程池中执行,不指定的话,通常任务是在 ForkJoinPool.commonPool() 线程池中执行的。

supplyAsync()使用

静态方法runAsync和supplyAsync允许我们相应地从Runnable和Supplier功能类型中创建CompletableFuture实例。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName() + " A");
    return "Hello";
});
System.out.println(Thread.currentThread().getName() + " B");
System.out.println(future.get());

结果:
main B
ForkJoinPool.commonPool-worker-1 A
Hello

thenRun()使用

在两个任务任务A,任务B中,如果既不需要任务A的值也不想在任务B中引用,那么你可以将Runnable lambda 传递给thenRun()方法。
在下面的示例中,在调用future.get()方法之后,我们只需在控制台中打印一行:
模板

CompletableFuture.runAsync(() -> {}).thenRun(() -> {}); 
CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {});
  • 第一行用的是 thenRun(Runnable runnable),任务 A 执行完执行 B,并且 B 不需要 A 的结果。
  • 第二行用的是 supplyAsync(Supplier<U> supplier),任务 A 执行完执行 B,会返回resultA,但是 B 不需要 A 的结果。

实战

CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName() + " A");
    return "Hello";
}).thenRun(() -> {
    System.out.println(Thread.currentThread().getName() + " Computation finished.");
});
System.out.println(Thread.currentThread().getName() + " B");
System.out.println(completableFuture.get());

结果:
ForkJoinPool.commonPool-worker-1 A
main Computation finished.
main B
null

thenAccept()使用

在两个任务任务A,任务B中,如果你不需要在Future中有返回值,则可以用 thenAccept方法接收将计算结果传递给它。最后的future.get()调用返回Void类型的实例。
模板

CompletableFuture.runAsync(() -> {}).thenAccept(resultA -> {}); 
CompletableFuture.supplyAsync(() -> "resultA").thenAccept(resultA -> {});
  • 第一行中,runAsync不会有返回值,第二个方法thenAccept,接收到的resultA值为null,同时任务B也不会有返回结果。
  • 第二行中,supplyAsync有返回值,同时任务B不会有返回结果。
    实战
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName() + " A");
    return "Hello";
});
CompletableFuture<Void> future = completableFuture.thenAccept(s -> {
    System.out.println(Thread.currentThread().getName() + " Computation returned: " + s);
});
System.out.println(Thread.currentThread().getName() + " B");
System.out.println(future.get());

结果:
ForkJoinPool.commonPool-worker-1 A
main Computation returned: Hello
main B
null

thenApply()使用

在两个任务任务A,任务B中,任务B想要任务A计算的结果,可以用thenApply方法来接受一个函数实例,用它来处理结果,并返回一个Future函数的返回值:

模板

CompletableFuture.runAsync(() -> {}).thenApply(resultA -> "resultB");
CompletableFuture.supplyAsync(() -> "resultA").thenApply(resultA -> resultA + " resultB");
  • 第二行用的是 thenApply(Function fn),任务 A 执行完执行 B,B 需要 A 的结果,同时任务 B 有返回值。

实战

CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
    ThreadUtil.sleep(1000L);
    System.out.println(Thread.currentThread().getName() + " A");
    return "Hello";
});
CompletableFuture<String> future = completableFuture.thenApply(s -> {
    System.out.println(Thread.currentThread().getName() + " B");
    return s + " World";
});
System.out.println(Thread.currentThread().getName() + " C");
System.out.println(future.get());

结果:
main C
ForkJoinPool.commonPool-worker-1 A
ForkJoinPool.commonPool-worker-1 B
Hello World

当然,多个任务的情况下,如果任务 B 后面还有任务 C,往下继续调用 .thenXxx() 即可。

thenCompose()使用

CompletableFuture API 的最佳场景是能够在一系列计算步骤中组合CompletableFuture实例。

这种组合结果本身就是CompletableFuture,允许进一步再续组合。这种方法在函数式语言中无处不在,通常被称为monadic设计模式。

简单说,Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。

在下面的示例中,我们使用thenCompose方法按顺序组合两个Futures。

请注意,此方法采用返回CompletableFuture实例的函数。该函数的参数是先前计算步骤的结果。这允许我们在下一个CompletableFuture的lambda中使用这个值:

CompletableFuture<String> completableFuture = CompletableFuture
        .supplyAsync(() -> {
            ThreadUtil.sleep(1000L);
            System.out.println(Thread.currentThread().getName() + " A");
            return "Hello";
        })
        .thenCompose(s -> CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " B");
            return s + " World";
        }));
System.out.println(Thread.currentThread().getName() + " C");
System.out.println(completableFuture.get());

结果:
main C
ForkJoinPool.commonPool-worker-1 A
ForkJoinPool.commonPool-worker-1 B
Hello World

该thenCompose方法连同thenApply一样实现了结果的合并计算。但是他们的内部形式是不一样的,它们与Java 8中可用的Stream和Optional类的map和flatMap方法是有着类似的设计思路在里面的。

两个方法都接收一个CompletableFuture并将其应用于计算结果,但thenCompose(flatMap)方法接收一个函数,该函数返回相同类型的另一个CompletableFuture对象。此功能结构允许将这些类的实例继续进行组合计算。

thenCombine()、thenAcceptBoth()使用

取两个任务的结果。
如果要执行两个独立的任务,并对其结果执行某些操作,可以用Future的thenCombine方法。

模板

CompletableFuture<String> cfA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture<String> cfB = CompletableFuture.supplyAsync(() -> "resultB");
cfA.thenAcceptBoth(cfB, (resultA, resultB) -> {});
cfA.thenCombine(cfB, (resultA, resultB) -> "result A + B");

实战

CompletableFuture<String> completableFuture = CompletableFuture
        .supplyAsync(() -> {
            ThreadUtil.sleep(1000L);
            System.out.println(Thread.currentThread().getName() + " A");
            return "Hello";
        })
        .thenCombine(CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " B");
            return " World";
        }), (s1, s2) -> {
            System.out.println(Thread.currentThread().getName() + " C");
            return s1 + s2;
        });
System.out.println(Thread.currentThread().getName() + " D");
System.out.println(completableFuture.get());

结果:
ForkJoinPool.commonPool-worker-2 B
main D
ForkJoinPool.commonPool-worker-1 A
ForkJoinPool.commonPool-worker-1 C
Hello World

更简单的情况是,当你想要使用两个Future结果时,但不需要将任何结果值进行返回时,可以用thenAcceptBoth,它表示后续的处理不需要返回值,而 thenCombine 表示需要返回值:

CompletableFuture<Void> completableFuture = CompletableFuture
        .supplyAsync(() -> {
            ThreadUtil.sleep(1000L);
            System.out.println(Thread.currentThread().getName() + " A");
            return "Hello";
        })
        .thenAcceptBoth(CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " B");
            return " World";
        }), (s1, s2) -> {
            System.out.println(Thread.currentThread().getName() + " C," + s1 + s2);
        });
System.out.println(Thread.currentThread().getName() + " D");
System.out.println(completableFuture.get());

结果:
ForkJoinPool.commonPool-worker-2 B
main D
ForkJoinPool.commonPool-worker-1 A
ForkJoinPool.commonPool-worker-1 C,Hello World
null

thenApply()和thenCompose()之间的区别

在前面的部分中,我们展示了关于thenApply()和thenCompose()的示例。这两个API都是使用的CompletableFuture调用,但这两个API的使用是不同的。

thenApply()

此方法用于处理先前调用的结果。但是,要记住的一个关键点是返回类型是转换泛型中的类型,是同一个CompletableFuture。

因此,当我们想要转换CompletableFuture 调用的结果时,效果是这样的 :

CompletableFuture<Integer> finalResult = compute().thenApply(s-> s + 1);

thenCompose()

该thenCompose()方法类似于thenApply()在都返回一个新的计算结果。但是,thenCompose()使用前一个Future作为参数。它会直接使结果变新的Future,而不是我们在thenApply()中到的嵌套Future,而是用来连接两个CompletableFuture,是生成一个新的CompletableFuture:

CompletableFuture<Integer> computeAnother(Integer i){
    return CompletableFuture.supplyAsync(() -> 10 + i);
}
CompletableFuture<Integer> finalResult = compute().thenCompose(this::computeAnother);

因此,如果想要继续嵌套链接CompletableFuture 方法,那么最好使用thenCompose()。

并行运行多个任务

当我们需要并行执行多个任务时,我们通常希望等待所有它们执行,然后处理它们的组合结果。

该CompletableFuture.allOf静态方法允许等待所有的完成任务:

API

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs){...}

实战

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Beautiful");
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(future1, future2, future3);
// ...
combinedFuture.get();
assertTrue(future1.isDone());
assertTrue(future2.isDone());
assertTrue(future3.isDone());

请注意,CompletableFuture.allOf()的返回类型是CompletableFuture <Void>。这种方法的局限性在于它不会返回所有任务的综合结果。相反,你必须手动从Futures获取结果。幸运的是,CompletableFuture.join()方法和Java 8 Streams API可以解决:

String combined = Stream.of(future1, future2, future3)
  .map(CompletableFuture::join)
  .collect(Collectors.joining(" "));
assertEquals("Hello Beautiful World", combined);

CompletableFuture 提供了 join() 方法,它的功能和 get() 方法是一样的,都是阻塞获取值,它们的区别在于 join() 抛出的是 unchecked Exception。这使得它可以在Stream.map()方法中用作方法引用。

异常处理

说到这里,我们顺便来说下 CompletableFuture 的异常处理。这里我们要介绍两个方法:

public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn);
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);

看下代码

CompletableFuture.supplyAsync(() -> "resultA")
    .thenApply(resultA -> resultA + " resultB")
    .thenApply(resultB -> resultB + " resultC")
    .thenApply(resultC -> resultC + " resultD");

上面的代码中,任务 A、B、C、D 依次执行,如果任务 A 抛出异常(当然上面的代码不会抛出异常),那么后面的任务都得不到执行。如果任务 C 抛出异常,那么任务 D 得不到执行。

那么我们怎么处理异常呢?看下面的代码,我们在任务 A 中抛出异常,并对其进行处理:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException();
}).exceptionally(ex -> "errorResultA")
        .thenApply(resultA -> resultA + " resultB")
        .thenApply(resultB -> resultB + " resultC")
        .thenApply(resultC -> resultC + " resultD");

System.out.println(future.join());

上面的代码中,任务 A 抛出异常,然后通过 .exceptionally() 方法处理了异常,并返回新的结果,这个新的结果将传递给任务 B。所以最终的输出结果是:errorResultA resultB resultC resultD

参考

Java CompletableFuture 详解
CompletableFuture基本用法
使用了CompletableFuture之后,程序性能提升了三倍
编程老司机带你玩转 CompletableFuture 异步编程

【JAVA8】CompletableFuture使用详解 CompletableFuture!让你的代码免受阻塞之苦! 灵活的使用`CompletableFuture`可以让我们感受java8的异步编程之美! 阅读详情

相关推荐

【Java】Java8新特性:CompletableFuture

CompletableFuture Java CompletableFuture 详解

flora 1088

Java8异步利器:CompletableFuture全网最全使用教程

点击上方“Java基基”,选择“设为星标”做积极的人,而不是积极废人!每天14:00更新文章,每天掉亿点点头发...源码精品专栏原创 | Java 2021超神之路,很肝~中文详细注释的开源项目RPC 框架 Dubbo 源码解析网络应用框架 Netty 源码解析消息中间件 RocketMQ 源码解析数据库中间件 Sharding-JDBC 和 MyCAT 源码解析作业调度中间件 Elast...

weixin_44421461的博客 1458

在 Java 中实现异步编程:CompletableFuture 使用指南

CompletableFuture 是 Java 8 引入的一个类,位于 java.util.concurrent 包中。它实现了 Future 和 CompletionStage 接口,用于表示一个异步计算的过程,并且提供了丰富的 API 来链式调用和组合多个异步操作。与传统的 Future 相比,CompletableFuture 不仅可以获取异步任务的结果,还能更加灵活地处理任务的完成回调、异常处理以及多个任务之间的依赖关系等。

shrgegrb的博客 1180

java多线程与并发原理学习笔记(七)java8异步神器CompletableFuture

是java8提供的一个异步编程类,提供了一系列的异步编程方法。它同时实现了Future和接口。所以不但包含了Future的功能特性,而且还实现了接口定义的任务编排的方法,可以实现不同任务的运行顺序、规则以及方式。

飞!!!的博客 1619

java8--新特性CompletableFuture多线程

java8新特性CompletableFuture多线程 参考文章: http://javadaily.cn/articles/2021/03/18/1616064645189.html https://www.liaoxuefeng.com/wiki/1252599548343744/1306581182447650 CompletableFuture可以指定异步处理流程: thenAccept()处理正常结果; exceptional()处理异常结果; thenApplyAsync()用于串行化另一个C

专搞技术的小兔子 1148

Java 8的新特性:Lambdas和CompletableFuture解析

1.背景介绍 Java 8是Java语言的一个重要版本,它引入了许多新的特性,这些特性使得Java语言更加强大和灵活。其中,Lambdas和CompletableFuture是Java 8最重要的两个新特性之一。Lambdas是Java 8中引入的一种新的函数式编程特性,它使得Java代码更加简洁和易读。CompletableFuture是Java 8

AI天才研究院 1046

Java8新特性之CompletableFuture

书籍原文:https://tuonioooo.gitbooks.io/java-concurrent/content/javautilconcurrentbao-jiang-jie/java8xin-te-xing-zhi-completablefuture.html

tuonioooo 279

使用mixamo导入人物动作到unity

导入的材质是withoutSkin时选择Create From Other Model + 选择带withSkin里的。方案二:Textures选择Extract Textures,将文件存放到喜欢的位置(不行继续方案三)2.动画没有动:开个Animation窗口,选择人物的Prefab(Hierachy),查看窗口左侧。方案三:选中素材自带或者导出的Material,接着方案一选择 选中的materials进行转化。选 withSkin,剩下的动作选without Skin)(角色发生位移时出现选项)

big_good_boy的博客 1635

Java8新特性CompletableFuture详解

thenCombine()被用来当两个独立的Future都完成的时候,用来做一些事情。CompletableFuture 是Java 8 中引入的 Java Future API的扩展,用于 Java 中的异步编程,它可以使我们的任务运行在与主线程分离的其他线程中,并通过回调在主线程中得到异步任务执行状态,包括是否完成,是否异常等信息。我们 使用 thenCompose()组合两个独立的future,假设我们想从一个远程API中获取一个用户的信息,当用户信息可用时,我们想从另外一个服务中获取另外的信息。

qq_42077317的博客 1927

wsl2内核版本和模块版本不一致解决方法

** /lib/modules/…build: No such file or directory错误解决办法这篇文章历时1个月整,从4月5日一直到5月5日,这一问题才得以解决。

m0_60634555的博客 5103

Multisim 11.0的安装破解汉化步骤

15:打开注册机,选择管理员运行,默认第一项,选择生成文件路径,这里不要随意选择,要找到你安装Multisim安装路径下Shard下的License Manager路径下,否则会报错,破解失败。14:后面会出现一个更新提示,选择否,然后选择重启电脑后,先不要选择重启电脑,在这里出现了问题(可能敲击回车,默认去重启电脑了),所以我说不要重启电脑,重启破解时右下角出现这,点击否。17:重启电脑,如果前面重启过,那可以跳过重启,应用程序下找到下图所示,启动。16:点击确认覆盖,路径下生成了该文件。

qq_53260940的博客 2371

【Java 8 新特性】Java CompletableFuture supplyAsync()详解

【Java 8 新特性】Java CompletableFuture supplyAsync()

qq_45721579的博客 8637

使用python排八字 计算八字的相合相冲五行分值等 ...

简介 八字术是一种古老的算命术。它运用阴阳、五行、干支、八卦等理论,把人的出生年月日时转化为八个字,进而推算人的富贵贫贱、祸福吉凶、穷通寿天、名利地位、家庭六亲等方面的情况 八字源自古代的中国,是一种利用天干和地支来准确记录年、月、日、时的方式,由“年干,年支”、“月干,月支”、“日干,日支”、“时干,时支”,共八个干支所组成(共八个字),年、月、日、时...

weixin_33717298的博客 5310

Java8新特性 CompletableFuture

Java8新特性 CompletableFuture

风小猪的博客 659

豆包大模型API调用两种方法

ps: 因为是自用,所以这里图方便采用硬编码密钥的形式;如果有需求/安全风险,建议参考官方教程设置ARK_API_KEY环境变量的方式。坏处是:看别人的教程走了一大圈弯路hh;基础调用的话方法一就可以满足了,也比较方便快捷,容易上手。将 <Model> 替换为 Endpoint ID即可。创建就按照提示填写信息就行,需要实名认证,有免费额度。绿框框里就是Endpoint ID。

kkkkkkkkkasey的博客 1万+

用Linkboy玩转Arduino:3小时搞定智能倒车雷达(附仿真文件下载)

本文详细介绍了如何利用Linkboy图形化编程工具,在3小时内快速搭建一个基于Arduino的智能倒车雷达项目。通过仿真环境,初学者无需硬件即可学习超声波测距原理、报警逻辑设计,并完成从虚拟仿真到实体硬件的完整流程。文章提供了清晰的步骤、调试技巧及功能扩展思路,并附有仿真文件,是入门Arduino和智能硬件制作的绝佳实践指南。

weixin_29159441的博客 403

【Java 8 新特性】Java CompletableFuture supplyAsync()

Java CompletableFuture supplyAsync与 thenApply() 一起使用自定义执行器 Executor与 whenComplete() 一起使用与流(Stream)一起使用参考文献 在此页面上,我们将提供Java CompletableFuture.supplyAsync()示例。 supplyAsync()是Java 8中引入的CompletableFuture静态方法。 方法supplyAsync()完成在ForkJoinPool.commonPool()或给定的Exec

猫巳的博客 3万+

VMware使用Windows、WinXP等虚拟机上不了网的解决办法(管用)

这个情况一般出现在可以插网线的电脑上,比如台式机和有网线插口的笔记本。需要转换器才能插网线的笔记本一般不会出现这种情况. 查看主机的IP地址和虚拟机的IP地址是否在同一网段,如果在的话,虚拟机就能上网了!!(备注:虚拟机中的IP地址一定要和主机中的用来上网的那个连接的IP地址相同哦)。

爱吃雪糕的小布丁的博客 1万+

【Java 8 新特性】Java 8 CompletableFuture 示例

Java 8 CompletableFuture 示例CompletableFuture.supplyAsyncCompletableFuture.thenApplyCompletableFuture.joinCompletableFuture.thenAcceptCompletableFutureTwoExample.javaCompletableFuture.whenCompleteCompletableFuture.getNow参考文献 java.util.concurrent.Completable

猫巳的博客 1538

【OpenClaw从入门到精通】第34篇:医疗AI的“Agent OS”时代:基于WiseClaw构建医院专属智能体平台(2026实测版)

2026年医疗AI迈入实战落地新阶段,全球首个医疗OpenClaw平台WiseClaw正式发布,香港中文大学(深圳)医院完成业内首个私有化部署。本文基于真实医疗场景需求与公开技术资料,系统拆解医疗智能体平台的建设逻辑:从医疗Agent OS的五大核心特性切入,详解门诊病历质控、用药审查、医院运行管理三大临床场景的智能体搭建流程,提供完整的私有化部署代码、安全配置方案与实操演示。文章兼顾新手入门与进阶需求,严格遵循医疗合规要求,强调“只读不写+人工复核”原则,所有方案均经过虚拟实测验证。

专注于人工智能、软件开发、工控自动化、工厂数字化及智能化等领域,希望和大家共同进步! 788

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Java8新特性整理之CompletableFuture

Java8新特性整理之CompletableFuture Future 接口的局限性 Future接口可以构建异步应用,但依然有其局限性。它很难直接表述多个Future 结果之间的依赖性。实际开发中,我们经常需要达成以下目的: 将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。 等待 Future 集合中的所有任务都完成。 仅等待 Future集合中最快结束...

weixin_42040639的博客 413

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

java1.8新特性 CompletableFuture 使用案例详解

CompletableFuture方法详解 1、runAsync 和 supplyAsync方法 CompletableFuture 提供了四个静态方法来创建一个异步操作。 public static CompletableFuture<Void> runAsync(Runnable runnable); public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);.

李虹柏的博客 1396

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

Java8新特性之CompletableFuture详解

jdk1.8中加入了CompletableFuture类来辅助实现异步编程。上一节中提到了线程池执行异步方法返回Fufure实例,但是当执行多个异步任务时,如何判断这些任务都已执行完毕则没有好的办法,只能通过定时轮询去判断这些任务isDone(),并且也需要创建线程池来实现。而新的CompletableFufure类则对异步编程提供了一些很不错的方法。如下: 静态方法: C

fw118958的专栏 1万+

Java8的新特性——Stream与completableFuture详解

具体来说,Fork/Join 框架使用了一个工作窃取算法,当某个线程完成任务时,它可以“窃取”其他线程的任务来处理。当任务完成时,无法直接通知主线程,这会导致回调地狱的问题。如果需要在任务完成后执行某些操作,通常需要在主线程中进行轮询,检查任务是否完成,这种方法繁琐且不优雅。时,我们可以轻松定义任务完成后的操作,而无需阻塞当前线程,提升了代码的效率和可维护性。来说,提交的任务在一个独立的线程中执行,主线程可以继续工作,而不是被阻塞。的一个扩展,提供了更强大的功能,允许我们以非阻塞的方式处理异步计算结果。

m0_53926113的博客 1528

【Java 8 新特性】Java CompletableFuture thenApply()

Java CompletableFuture继承了CompletionStage和Future接口。 CompletableFuture.thenApply继承自CompletionStage。 thenApply返回一个新的CompletionStage,当该阶段正常完成时,将使用该阶段的结果作为所提供函数的参数来执行该过程。 从Java文档中找到thenApply的方法声明。 <U> CompletionStage<U> thenApply(Function<? super

猫巳的博客 7170
上一篇: Java并发编程 之Future、ListenableFuture
下一篇: Java开发 之文件操作
yyangqqian
博客等级 码龄15年 406粉丝 406原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值