我们知道,在java中如果实现异步编程,可以使用线程池来实现,也可以使用老的future来实现;但是他们在使用中会有很多的不足,比如:等待执行结果时我们必须要使用其他的实现方案或者使用future的get()方法;无法解决异步线程间的依赖关系;多个future不能合并起来;没有异常处理机制。以上这些痛点在CompletableFuture中都已经给出了很好的解决。下面通过api的使用来讲解相关内容:
- 提交任务:
向CompletableFuture中提交任务有两种方式,分别是supplyAsync提交的任务有返回值和runAsync提交的任务没有返回值。
// 提交有返回值任务
CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
return "Hello,world!";
}
});
System.out.println(future.get());
// 提交没有返回值任务
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
System.out.println("Hello,world!");
}
}).get();
CompletableFuture默认使用的是ForkJoinPool.commonPool
线程池,线程大小是与CPU核数一致的,这样就有可能不满足业务的需求,我们可以使用自己定义的线程池,
// 自定义线程池
ForkJoinPool pool = new ForkJoinPool(64);
// 提交有返回值任务并指定线程池
CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
System.out.println(Thread.currentThread().getName());
return "Hello,world!";
}
}, pool);
System.out.println(future.get());
// 提交没有返回值任务并指定线程池
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println("Hello,world!");
}
}, pool).get();
- 等待结果:
CompletableFuture也可以像老的future那样通过get()方法等待结果,但这种方式会阻塞主线程的执行,这样的话异步执行跟同步执行就没有区别了。
CompletableFuture提供了多个获取结果的方法:thenApply()或thenApplyAsync() 等待future处理完成后执行,并且有执行结果返回;thenAccept()或thenAcceptAsync() 等待future处理完成后执行,该方法只消费上一个future的结果,没有返回值;thenRun()或thenRunAsync() 等待future处理完成后执行,它只是等待任务完成,并不会使用上一个future的结果,也没有结果的返回。注意每个api都有一个Async
结尾的方法,它表示在新的线程中执行,也可以指定新的线程池执行任务。示例如下:
// 自定义线程池
ForkJoinPool pool = new ForkJoinPool(64);
// 提交有返回值任务并指定线程池
CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
System.out.println("第一步:" + Thread.currentThread().getName());
return "Hello,world!";
}
}, pool).thenApply(new Function<String, String>() {
@Override
public String apply(String s) {
System.out.println("第二步:" + Thread.currentThread().getName());
return s + "--1";
}
}).thenApplyAsync(new Function<String, String>() {
@Override
public String apply(String s) {
System.out.println("第三步:" + Thread.currentThread().getName());
return s + "--2";
}
}, pool).thenAccept(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println("第四步:" + Thread.currentThread().getName());
System.out.println(s);
}
}).thenAcceptAsync(new Consumer<Void>() {
@Override
public void accept(Void unused) {
System.out.println("第五步:" + Thread.currentThread().getName());
}
}, pool).thenRun(new Runnable() {
@Override
public void run() {
System.out.println("第六步:" + Thread.currentThread().getName());
}
}).thenRunAsync(new Runnable() {
@Override
public void run() {
System.out.println("第七步:" + Thread.currentThread().getName());
}
}, pool);
- 多个不相关的CompletableFuture等待执行
在编程中经常会有这种场景,在一个请求中要获取多个不相关的数据,比如一个下单接口:我们要获取用户的身份信息,查询账户余额,查询其他认证信息。这些信息收集齐了并且校验没有问题后才会进行创建订单流程,这几个步骤是无关联的但是必须在下单前完成,这时就可以考虑使用下面的相关api来完成需求:
(1)等待两个不相关的任务执行完成才进行下一步任务,可以通过 thenCombine()或thenCombineAsync() 方法,它会在两个future都返回结果后进行下一步操作:
// 自定义线程池
ForkJoinPool pool = new ForkJoinPool(64);
// 开始时间
long start = System.currentTimeMillis();
// 任务1
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
// 等待1s后返回结果,模拟耗时任务
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello,world!";
}
}, pool);
// 任务2
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
// 等待500ms后返回结果,模拟耗时任务
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello,java!";
}
}, pool);
// 等待两个future执行完成后进行下一步操作
future1.thenCombine(future2, new BiFunction<String, String, String>() {
@Override
public String apply(String s, String s2) {
return s + " + " + s2;
}
}).thenAccept(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
System.out.println("use time : " + (System.currentTimeMillis() - start) + "ms");
}
}).get();
(2)等待多个任务执行完成才进行下一个任务,上面的方式就不能满足了,但是CompletableFuture已经为我们提供了allOf()方法来满足这种场景的需求,使用示例如下:
// 自定义线程池
ForkJoinPool pool = new ForkJoinPool(64);
// 开始时间
long start = System.currentTimeMillis();
// 任务1
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
// 等待1s后返回结果,模拟耗时任务
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello,world!";
}
}, pool);
// 任务2
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
// 等待500ms后返回结果,模拟耗时任务
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello,java!";
}
}, pool);
// 任务3
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
// 等待300ms后返回结果,模拟耗时任务
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello,boy!";
}
}, pool);
// 等待多个future都完成后进行下一步操作
CompletableFuture.allOf(future1, future2, future3).thenRun(new Runnable() {
@Override
public void run() {
try {
System.out.println(future1.get() + " + " + future2.get() + " + " + future3.get());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("use time : " + (System.currentTimeMillis() - start) + "ms");
}
}).get();
我这里只是简单的示例代码,其实我们可以将这些future存放在数组中,通过数组循环遍历使用:
// 等待多个future都完成后进行下一步操作
CompletableFuture[] futures = { future1, future2, future3 };
CompletableFuture.allOf(futures).thenRun(new Runnable() {
@Override
public void run() {
try {
StringBuilder builder = new StringBuilder();
for(int i = 0; i < futures.length; i++) {
builder.append(" + ").append(futures[i].get());
}
System.out.println(builder.substring(3));
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("use time : " + (System.currentTimeMillis() - start) + "ms");
}
}).get();
(3)两个任务只要有一个执行完成就可以进行下一步操作,可以使用 applyToEither() 或 applyToEitherAsync 、
acceptEither() 或 acceptEitherAsync() 、runAfterEither 或 runAfterEitherAsync 来实现,他们分别对应使用上一步的结果且有返回值、消费上一步的结果没有返回值、不使用上一步的结果且没有返回值。这几个api使用很相似,简单示例一个的使用方式:
future1.acceptEither(future2, new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
System.out.println("use time : " + (System.currentTimeMillis() - start) + "ms");
}
}).get();
(4)当任务数很多且只要有一个任务返回结果就可以进行下一步了,这时就可以使用anyOf()方法:
// 等待多个future结果,只要有一个完成就进行下一步操作
CompletableFuture[] futures = { future1, future2, future3 };
CompletableFuture.anyOf(futures).thenAccept(new Consumer<Object>() {
@Override
public void accept(Object obj) {
System.out.println(obj);
System.out.println("use time : " + (System.currentTimeMillis() - start) + "ms");
}
}).get();
- 异常处理
传统方式进行异步编程,对于异常处理没有很好的支持,我们通常需要使用其他的技术手段实现异常的优雅处理。而在CompletableFuture中对异常进行了很好的支持。
(1)使用exceptionally在程序执行过程中如果出现了异常转入异常处理逻辑,不会影响程序继续向下运行:
CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
int rs = 5 / 0;
return "Hello,world!";
}
}).exceptionally(new Function<Throwable, String>() {
@Override
public String apply(Throwable throwable) {
return "exception";
}
}).thenApply(new Function<String, String>() {
@Override
public String apply(String s) {
return "result data : " + s;
}
});
System.out.println("任务结果:" + future.get());
(2)使用handle()实现对异常的捕获,不论程序是否发生异常,handle()方法都会被执行到,我们可以通过判断是否抛出异常来做相应的处理:
CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
int rs = 5 / 0;
return "Hello,world!";
}
}).handle(new BiFunction<String, Throwable, String>() {
@Override
public String apply(String s, Throwable throwable) {
if(throwable == null) {
return s;
}
return "exception";
}
}).thenApply(new Function<String, String>() {
@Override
public String apply(String s) {
return "result data : " + s;
}
});
System.out.println("任务结果:" + future.get());
对比exceptionally()和handle()方法,exceptionally()方法在发生异常时返回的是异常内对应的值,我们得不到调用链上之前的运行结果;handle()方法在程序运行过程中发生异常时会把异常对象抛出同时也会把调用链上的执行结果返回,通过判断异常对象是否为空来确认是否发生了异常,由于有上一步的结果数据可以让程序继续运行。
- 等待结果
CompletableFuture在不阻塞等待结果时会一直向下运行,如果想在主线程中获取到任务的结果,只能通过阻塞方式拦截程序的运行。对于阻塞等待结果有两种方式:第一种是使用get()方法获取执行结果;另外一种是使用join()方法获取执行结果。从官方解释来看,join()方法更符合在流式编程中获取结果。get()方法有重载方法,可以指定等待结果的超时时间。
String rs1 = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
return "Hello,world!";
}
}).get();
System.out.println(rs1);
String rs2 = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
return "Hello,java!";
}
}).join();
System.out.println(rs2);
上面总结的内容基本涵盖了CompletableFuture全部内容,在进行异步编程时,CompletableFuture相比于老的Future更加好用,它提供了很多api处理常见的业务场景,结合lambda表达式和流式编程使得异步处理非常方便。