在 Java 中,如果需要异步执行任务,可以使用线程来实现,但是我们希望线程执行完之后可以获得执行结果,怎么实现呢?
JDK 1.5 中,引入了 Future 的概念,它可以结合 Callable 接口来获得线程异步执行完成之后的返回值,但它在使用上存在一定的局限性。所以在 JDK1.8 中引入了 CompletableFuture 组件,在 Future 的基础上提供了更加丰富和完善的功能。
Java5新加的一个接口,它提供了一种异步并行计算的功能,实现类
下面,通过代码来演示一下,如何使用:
class MyThread implements Callable {
@Override
public String call() throws Exception {
return "hello!";
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask ft = new FutureTask<>(new MyThread());
Thread t1 = new Thread(ft);
t1.start();
String s = ft.get(); // 获取异步线程的返回结果
System.out.println(s);
}
运行结果:
获取到了异步线程的返回结果“hello”。
结合线程池,可以大大提高程序的运行效率;
① get() 方法阻塞线程,一旦调用不见不散,需要等待异步线程执行完毕,返回处理结果,违背了异步线程提高效率的初衷;
② 如果异步线程耗时较久,主线程又不想等待太长时间,可以通过设置等待时间【get("x",TimeUnit.xx)】进行处理,过时不候,即当达到了设置的等待时间,异步线程仍没有返回结果,直接抛出异常;
③ 等待超时抛出异常的方式,不太优雅,会报出太多的异常日志,可以使用 isDone()方式来告知异步线程的处理状态,当处理完成后,告知主线程处理完成,但是主线程一直轮询等待会耗费无谓的 CPU 资源;
代码演示:
get()阻塞
class MyThread implements Callable {
@Override
public String call() throws Exception {
System.out.println("异步线程开始执行");
TimeUnit.SECONDS.sleep(5);
return "hello callable!!";
}
}
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
FutureTask futureTask = new FutureTask<>(new MyThread());
Thread thread = new Thread(futureTask);
thread.start();
String s = futureTask.get();
System.out.println("异步线程: " + thread.getName() + "返回的结果:" + s);
System.out.println("====== 主线程执行结束 ========");
long end = System.currentTimeMillis();
System.out.println("======== 共耗时 ============ " + (end-start) + "ms");
}
可以看到,当调用了异步线程获取返回结果的时候,影响了主线程的运行效率。
get(xx,xx) 等待超时,抛出异常
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
FutureTask futureTask = new FutureTask<>(new MyThread());
Thread thread = new Thread(futureTask);
thread.start();
// 主线程,只等待 3s,如果 3s没有响应,则抛出异常
String s = futureTask.get(3,TimeUnit.SECONDS);
System.out.println("异步线程: " + thread.getName() + "返回的结果:" + s);
System.out.println("====== 主线程执行结束 ========");
long end = System.currentTimeMillis();
System.out.println("======== 共耗时 ============ " + (end-start) + "ms");
}
isDone()轮询,浪费无谓的 CPU 资源
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
FutureTask futureTask = new FutureTask<>(new MyThread());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("====== 主线程执行结束 ========");
// isDone 轮询等待
while (true) {
if (futureTask.isDone()) {
String s = futureTask.get();
System.out.println("异步线程处理返回结果===== " + s);
break;
} else {
System.out.println("异步线程正在处理~~");
}
}
long end = System.currentTimeMillis();
System.out.println("======== 共耗时 ============ " + (end-start) + "ms");
}
通过上面的代码演示,可以得知,Future对于结果的获取不是很友好,只能通过阻塞或者轮询的方式得到任务的结果。显然,这是不够完美的,那么有没有一种更完美的解决方案呢?答案是有的,也就是 JDK1.8 中的 CompletableFuture,完美的解决了这些问题。
① 对于简单的业务场景,使用 Futrue 完成 OK;
② 对于复杂的,高并发的业务场景,我们需要有回调通知,当异步线程执行完毕后,主动通知主线程,而不是通过 isDone()轮询的方式去一直等待,那样大大的占用了CPU 资源;
③ 异步任务(Future + 线程池配置使用);
④ 多任务前后依赖组合处理,比如下一个任务,需要前一个任务的结果进行支撑,两个任务可以独立处理,但是又有依赖关系;
⑤ 对计算速度选最快等(并行的异步任务,哪个先完成,返回哪个结果);
Future的 get() 方法在计算完成之前会一直处于阻塞状态;
isDone()方法容易耗费 CPU 资源;
对于真正的异步处理,我们希望是可以通过传入回调函数,在 Future结束时自动调用该回调函数,这样我们就不用等待结果;
阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无畏的 CPU 资源,因此,JDK1.8 设计出 CompletableFuture。它提供了一种观察者模式类似的机制,可以让任务执行完后通知监听的一方。
CompletableFuture 扩展了 Futrue的所有功能,同时提供了其他更加强大的功能。
先来看一下类图:
在官方的 API 文档中,是不推荐直接使用无参构造方法来进行创建实例的。
共分为两类,一类是有返回值,一类是无返回值:
① runAsync 无返回值
② supplyAsync 有返回值
Executor 参数说明: 如果没有指定 Executor 的方法,直接使用默认的 ForkJoinPool.commonPool()作为它的线程池执行异步代码;如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码。
代码演示:
无返回值
public static void main(String[] args) throws Exception {
CompletableFuture completableFuture = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("task is over!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Void unused = completableFuture.get();
System.out.println(unused);
}
public static void main(String[] args) throws Exception {
// 自定义线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
CompletableFuture completableFuture = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("task is over!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
},pool);
Void unused = completableFuture.get();
System.out.println(unused);
// 使用完后进行关闭
pool.shutdown();
}
public static void main(String[] args) throws Exception {
// 自定义线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello supplyAsync";
},pool);
String result = completableFuture.get();
System.out.println(result);
// 使用完后进行关闭
pool.shutdown();
前面说了,CompletableFuture 是 Future的增强版,减少了阻塞和轮询,通过上面的代码,发现和 Future 并没有什么区别,那它是怎么做到的呢?来看下面的代码:
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(3);
CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 产生一个 随机数
int i = ThreadLocalRandom.current().nextInt(10);
return i;
},pool).whenComplete((v,e) -> {
// v: 上一步的返回结果,e: 异常信息
// 正常执行完毕,没有异常
if (e == null) {
System.out.println("异步线程执行完毕,进行回调======== " + v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println("异步线程出现异常了============ " + e.getCause() + "\t" + e.getMessage());
return null;
});
System.out.println(Thread.currentThread().getName() + " 主线程执行处理其他业务去了 ===========");
// 注意:使用默认的线程池 主线程不要 立刻结束,否则 CompletableFuture默认使用的线程池会立刻关闭
TimeUnit.SECONDS.sleep(3);
// 使用我们自己的线程池
pool.shutdown();
}
通过上面的代码,可以很明显的看出 CompletableFuture灵活了很多。
异步任务结束时,会自动回调某个对象的方法;
主线程设置好回调后,不再关系异步任务的执行,异步任务之间可以顺序执行;
异步任务出错时,会自动回调某个对象的方法;
join 和 get 对比
两者都是获取异步线程的返回值,区别在于:
get 在编译期间会抛出异常,需要进行声明式抛出,允许被中断,抛出 InterruptedException 异常;
join 在编译期间不会抛出异常,在运行时进行处理,不允许被中断;
allOf() 和 anyOf()
allOf: 接收多个 CompletableFuture 无返回值任务,当所有的任务执行结束后,返回一个新的 CompleatbleFuture 对象;
anyOf: 接收多个带有返回值的任务,当任何一个任务执行完成后,返回一个新的CompleatbleFuture 对象;
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture futureA = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
}, threadPool);
CompletableFuture futureB = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 20;
}, threadPool);
System.out.println(CompletableFuture.anyOf(futureA, futureB).join());
threadPool.shutdown();
电商比价需求分析:
① 同一款产品,同时搜索出同款产品 在各大电商平台的售价;
② 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少;
解决方案:
①中规中矩, 一个线程一步一步的去查;
②万箭齐发,异步线程,同时分开去查;
代码演示:
public class CompletableFutureTest {
static List netMalls = Arrays.asList(
new NetMall("JD"),
new NetMall("taobao"),
new NetMall("dangdang")
);
public static void main(String[] args) {
long start = System.currentTimeMillis();
List strings = getPrice(netMalls, "MySQL");
strings.stream().forEach(item -> System.out.println(item));
long end = System.currentTimeMillis();
System.out.println(" ======== 共耗时 ===== " + (end - start));
}
public static List getPrice(List malls, String productName) {
return malls.stream()
.map(item ->
String.format("《" + productName + "》" + " in %s price is %.2f", item.getNetMallName(), item.calcPrice(productName)))
.collect(Collectors.toList());
}
}
class NetMall {
@Getter
private String netMallName;
public NetMall(String netMallName) {
this.netMallName = netMallName;
}
public double calcPrice(String productName) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ThreadLocalRandom.current().nextDouble(5) *2 + productName.charAt(0);
}
}
第一种解决方案,满足了我们的需求,但是,不尽人意,此时,需要对功能进行性能优化,使用第二种解决方案进行优化:
public static void main(String[] args) {
long start = System.currentTimeMillis();
// 使用异步方式进行处理
List strings = getPriceByAsync(netMalls, "MySQL");
strings.stream().forEach(item -> System.out.println(item));
long end = System.currentTimeMillis();
System.out.println(" ======== 共耗时 ===== " + (end - start));
}
/**
* 异步处理
* @param malls
* @param productName
* @return
*/
public static List getPriceByAsync(List malls, String productName) {
return malls.stream()
.map(item ->
CompletableFuture.supplyAsync(
() -> String.format("《" + productName + "》" + " in %s price is %.2f", item.getNetMallName(), item.calcPrice(productName))))
.collect(Collectors.toList())
.stream()
.map(cf -> cf.join()).collect(Collectors.toList());
}
通过优化,发现性能上升显著,由原来的 3s 到现在的 1s,非常棒!当然,此案例中只有 3 家电商平台,在现实的业务场景中,远远不止这么几家,当集合的数量越大的时候,性能提升的越明显。
API内容较多,这里简单分为五组进行描述
① 获得结果和触发计算;
② 对计算结果进行处理;
③ 对计算结果进行消费;
④ 对计算速度进行选用;
⑤ 对计算结果进行合并;
获得结果:get() / get(x,TimeUtil.xx) / join() / getNow("xxx")
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
CompletableFuture supplyAsync = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "123";
});
// // 线程阻塞,不见不散
// String result = supplyAsync.get();
// // 与 get 的区别在于不会在编译期间显示的声明异常;
// String res = supplyAsync.join();
// // 给定一个等待时间,在等待时间内,不返回结果,则抛出异常
// String r = supplyAsync.get(1L, TimeUnit.SECONDS);
TimeUnit.SECONDS.sleep(3L);
// 在调用返回结果的时候,如果线程没有处理完,则使用给定的默认值,不会阻塞线程
String defaultValue = supplyAsync.getNow("返回默认值");
}
}
触发计算: completa(T value) 是否打断 get方法,立即获取括号值
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
CompletableFuture supplyAsync = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "123";
});
// TimeUnit.SECONDS.sleep(3L);
// 如果异步线程没有执行完毕,则直接打断,获取给定的值
System.out.println(supplyAsync.complete("aaa"));
System.out.println(supplyAsync.join());
}
}
对计算结果进行处理:thenApply / handle 计算结果存在依赖关系,两个线程串行化,区别在于两者对异常的处理稍有不同,下面通过代码来进行演示。
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "111");
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
}, threadPool).thenApply(f -> {
// 故意抛出一个异常
int i= f/0;
System.out.println(Thread.currentThread().getName() + "222");
return f + 2;
}).thenApply(f -> {
System.out.println(Thread.currentThread().getName() + "333");
return f + 3;
}).exceptionally(e -> {
e.printStackTrace();
System.out.println(e.getMessage());
return null;
});
System.out.println(future.join());
threadPool.shutdown();
}
}
正常运行下
thenApply 对异常的处理 ,出现异常,直接终止
handle 正常的运行下,和 thenApply 一样,主要来看一下异常的处理:
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "111");
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
}, threadPool).handle((f,e) -> {
// 故意抛出一个异常
int i= f/0;
System.out.println(Thread.currentThread().getName() + "222");
return f + 2;
}).handle((f,e) -> {
System.out.println(Thread.currentThread().getName() + "333");
return f + 3;
}).exceptionally(e -> {
e.printStackTrace();
System.out.println(e.getMessage());
return null;
});
System.out.println(future.join());
threadPool.shutdown();
}
}
通过异常来看,handle 有异常的时候,会跳过,继续往下执行。
对计算结果进行消费: thenAccept 接收任务的处理结果,并消费处理,无返回结果
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "111");
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
}, threadPool).handle((f,e) -> {
System.out.println(Thread.currentThread().getName() + "222");
return f + 2;
}).thenAccept(System.out::println);
threadPool.shutdown();
}
}
thenRun / thenAccept / thenApply 三者的执行顺序
API 接口 | 执行顺序 |
thenRun | 任务 A执行完毕执行 B,并且 B不需要 A的就结果 |
thenAccept | 任务 A执行完毕执行 B,B需要 A的结果,但是任务 B无返回值 |
thenApply | 任务 A执行完毕执行 B,B需要 A的结果,同时任务 B 有返回值 |
关于异步线程的线程池
1、没有传入自定义线程池, 都用默认线程池ForkJoinPool;
2、传入了一个自定义线程池,如果你执行第一个任务的时候,传入了一个自定义线程池:
调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池3、备注
有可能处理太快,系统优化切换原则,直接使用main线程处理
其它如:thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,它们之间的区别也是同理
对计算速度选用: applyToEither 谁先完成,用谁的结果。
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture A = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "A";
}, threadPool);
CompletableFuture B = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "B";
}, threadPool);
CompletableFuture future = A.applyToEither(B, f -> "Get is " + f);
System.out.println(future.join());
threadPool.shutdown();
}
}
对结果集合并: thenCombine 两个 completionStage任务都完成后,最终将江哥任务的结果一起交给 thenCombine 来处理,先完成的等待后完成的
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture futureA = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
}, threadPool);
CompletableFuture futureB = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 20;
}, threadPool);
CompletableFuture
CompletableFuture 还有其他很多功能,小伙伴们可以去看一下其他相关的 API 接口,这里不做过多描述。
通过这篇文章,相信大家已经对 Java8 中的 CompletableFuture 有了深入掌握。
在日常的开发工作中,希望能合理运用到项目上,提升服务的性能!