Java8特性 - CompletableFuture的基础使用
一. CompletableFuture的简单介绍
CompletableFuture
是Java8里面推出的新特性,其实现了Future
、CompletionStage
接口,可以表示一个异步计算的结果。通过调用get()
方法进入阻塞直到结果返回。
对于原生的Future
类,我们只能通过轮询或者阻塞的方式来获取其结果,而不断的轮询又会消耗CPU,同时只能在获得结果之后手动做逻辑处理。而CompletableFuture
拓展了Future
,简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了各种转换以及组合的相关API。
用CompletableFuture
去执行一个异步代码,有两个最常见的方法:
supplyAsync
:异步、有返回值。runAsync
:异步:无返回值(Void类型)(因为接收的参数是一个Runnable)
先来一个简单的异步Demo,了解下CompletableFuture是怎么使用的:
@Test
public void test() throws Exception {
long start = System.currentTimeMillis();
// 有返回值
CompletableFuture<String> c = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("-----supplyAsync-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello";
});
// 无返回值
CompletableFuture<Void> c2 = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("-----runAsync-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 阻塞等待所有异步任务结束
CompletableFuture.allOf(c, c2).get();
long end = System.currentTimeMillis();
System.out.println("主线程执行结束,消耗时长:" + (end - start) + "ms");
}
最终程序的执行结果如下,可见程序并不是串行的等待了5秒,而是通过异步的方式,等待了3秒(消耗时间最长的任务)。
上述两种方法,都有其对应的重载方法,可以让我们手动传入一个线程池,用于创建执行异步任务的线程,以runAsync
为例:
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) {
return asyncRunStage(screenExecutor(executor), runnable);
}
1.1 主线程和守护线程
CompletableFuture
在默认的情况下(不传入指定线程池),异步任务采用的线程是通过ForkJoinPool
创建出来的子线程,是守护线程。
验证如下:
@Test
public void test2() throws Exception {
CompletableFuture<String> c = CompletableFuture.supplyAsync(() -> {
System.out.println("supplyAsync" + (Thread.currentThread().isDaemon() ? "是" : "不是") + "守护线程");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("supplyAsync执行结束");
return "Hello";
});
c.get();
System.out.println("主线程执行结束");
}
结果如下:
守护线程:
Java的线程分为两种:用户线程、守护线程。默认情况下创建出来的线程都是用户线程。守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务,例如我们Java自带的GC功能,就是通过守护线程来执行的。
需要注意的点:
- 守护线程会等到主线程执行完毕后也跟着结束。
- 对主线程来说, 运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕, 主线程才算运行完毕。
那么问题随之而来,加入我把上述代码改为:
@Test
public void test3() throws Exception {
long start = System.currentTimeMillis();
// 有返回值
CompletableFuture<String> c = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("-----supplyAsync-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello";
});
// 无返回值
CompletableFuture<Void> c2 = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("-----runAsync-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.currentTimeMillis();
System.out.println("主线程执行结束,消耗时长:" + (end - start) + "ms");
}
结果将会是:
很明显,可以发现,我两个异步任务里面的输出语句都没有执行到,而对于主线程而言,执行了61ms就结束了,那么主线程关闭,CompletableFuture
通过默认方式创建的异步任务(非守护线程)也会随之结束,哪怕没有执行完毕。
因此想要解决这样的问题,就应该让主线程进入阻塞状态,等待守护线程全部执行完毕,也就是get()
方法。或者保证执行异步任务的期间,主线程内还有非守护线程还在执行。或是传入自己指定的线程池,来代替默认的ForkJoinPoll
。
例如:
@Test
public void test3() throws Exception {
long start = System.currentTimeMillis();
// 有返回值
CompletableFuture<String> c = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("-----supplyAsync-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello";
});
// 无返回值
CompletableFuture<Void> c2 = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("-----runAsync-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 一般我们创建出来的线程都是用户线程
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(()->{
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).get();
long end = System.currentTimeMillis();
System.out.println("主线程执行结束,消耗时长:" + (end - start) + "ms");
}
执行结果如下,可以见到异步任务中的输出都有了。
二. 基础的运算
2.1 thenAccept
thenAccept
:当原来的CompletableFuture
计算完后,将结果传递给函数fn
,将fn
的结果作为新的CompletableFuture
并计算新的结果。案例如下:
@Test
public void test4() {
CompletableFuture<String> c1 = CompletableFuture.supplyAsync(() -> {
System.out.println("今天天气不错");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "衣服";
});
CompletableFuture<Void> end = c1.thenAccept(data -> {
System.out.println("我去买了" + data);
});
end.join();
}
结果:
2.2 whenComplete
当计算完成或者抛出异常后,我们可以调用whenComplete
来执行指定的动作。
案例如下:
@Test
public void test4() {
CompletableFuture<String> c1 = CompletableFuture.supplyAsync(() -> {
System.out.println("今天天气不错");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "衣服";
});
CompletableFuture<String> end = c1.whenComplete((pre, cur) -> {
System.out.println("我去买了" + pre);
});
end.join();
}
结果和thenAccept
一致,但是两者区别如下:
thenAccept
:无返回值(Void),返回一个新的CompletableFuture
。whenComplete
:有返回值,返回的CompletableFuture
的返回与原有返回结果的是同一个(已做修改,原表述错误,感谢评论区turbo强)。
(增强案例)证明如下:
@org.junit.Test
public void test4() throws ExecutionException, InterruptedException {
CompletableFuture<String> c1 = CompletableFuture.supplyAsync(() -> {
System.out.println("今天天气不错");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "衣服";
});
CompletableFuture<String> c2 = c1.whenComplete((pre, cur) -> {
System.out.println("我去买了" + pre);
});
c2.join();
System.out.println("Future1的结果:" + c1.get());
System.out.println("Future2的结果:" + c2.get());
}
结果如下:
2.3 thenCompose
thenCompose
是用于组合CompletableFuture
,并返回一个新的CompletableFuture
,案例如下:
@Test
public void test4() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 100;
});
CompletableFuture<String> f = future.thenCompose( i -> {
return CompletableFuture.supplyAsync(() -> {
return (i * 10) + "";
});
});
System.out.println(f.get());
}
返回结果: