Future回顾
在引入CompletableFuture实现类之前,需要对Future接口作一个回顾。FutureTask是该接口的一个实现类,该接口定义了操作异步任务执行的方法,比如获取异步任务执行的结果、取消异步任务、判断异步任务是否被取消、判断异步任务是否执行完毕等等。
Future接口架构及其使用演示
我们知道,异步多线程任务执行并且有返回值,具有三个特点:多线程、有返回值、异步任务。具体代码实现方面,可以采用Runnable接口+Callable接口+Future接口和FutureTask实现类来实现,以下是Future接口的架构树结构:
/**
* @Author:zxp
* @Description:Future接口的演示
* @Date:21:41 2024/4/27
*/
public class FutureTest {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 8,
TimeUnit.SECONDS, new LinkedBlockingDeque<>(10),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
List<FutureTask<Map<Integer,Integer>>> futureTaskList=new ArrayList<>();
HashMap<Integer,Integer> totalMap=new HashMap<>();
for(int i=0;i<3;i++){
int cur=i;
FutureTask<Map<Integer, Integer>> mapFutureTask = new FutureTask<>(() -> getLabelBOList(cur));
futureTaskList.add(mapFutureTask);
executor.submit(mapFutureTask);
}
futureTaskList.forEach(mapFutureTask -> {
Map<Integer, Integer> integerIntegerMap = mapFutureTask.get();
totalMap.putAll(integerIntegerMap);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
});
totalMap.forEach((key,value)->{
System.out.println("key "+key+" value "+value);
});
executor.shutdown();
}
public static Map<Integer, Integer> getLabelBOList(Integer id){
Map<Integer,Integer> map=new HashMap<>();
map.put(id,id);
return map;
}
}
在这里,我们创建了一个ThreadPoolExecutor线程池对象,用于创建执行异步任务的子线程。执行结果如下:
Future接口实现异步多线程任务优缺点分析
我们使用线程池配合FutureTask实现异步多线程任务的执行,显著了提高了程序的运行效率。刚才的演示是正常的情况,每个异步任务执行都非常快,基本感受不到停顿,但是实际上,当调用get()方法企图获取执行结果时,有这样一个特点,那就是必须要接收到结果,如果没有执行完那么程序就会等待,甚至堵塞,这也是get()方法的一个缺点。虽然Future接口定义了isDone()方法用于判断线程是否执行完成,但是轮询的方式会占用资源,造成cpu空转,显然是不可取的。下面对上面的代码作修改来演示这一效果:
/**
* @Author:zxp
* @Description:Future接口的演示
* @Date:21:41 2024/4/27
*/
public class FutureTest {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 8,
TimeUnit.SECONDS, new LinkedBlockingDeque<>(10),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
List<FutureTask<Map<Integer,Integer>>> futureTaskList=new ArrayList<>();
HashMap<Integer,Integer> totalMap=new HashMap<>();
for(int i=0;i<3;i++){
int cur=i;
FutureTask<Map<Integer, Integer>> mapFutureTask = new FutureTask<>(() -> getLabelBOList(cur));
futureTaskList.add(mapFutureTask);
executor.submit(mapFutureTask);
}
futureTaskList.forEach(mapFutureTask -> {
try {
while (true){
if(mapFutureTask.isDone()){
Map<Integer, Integer> integerIntegerMap = mapFutureTask.get();
totalMap.putAll(integerIntegerMap);
break;
}
else {
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("询问是否完成");
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
});
totalMap.forEach((key,value)->{
System.out.println("key "+key+" value "+value);
});
executor.shutdown();
}
public static Map<Integer, Integer> getLabelBOList(Integer id){
try {
System.out.println("进入休眠"+(id+1)+"秒钟");
Thread.sleep((id+1)*1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Map<Integer,Integer> map=new HashMap<>();
map.put(id,id);
return map;
}
}
我们在需要执行的异步任务中加入了短暂的睡眠时间,并且使用轮询来判断任务是否执行完毕,演示效果如下:
可以看到由于睡眠时间的存在,一开始的询问确实没有得到任务执行结果,造成cpu的空转。
复杂任务Future捉襟见肘
如果是比较简单的业务场景,线程池配合FutureTask执行异步多线程任务完全顶得住。但是如果任务比较复杂,任务并不能在很短时间结束,这时候调用get()方法获取任务的执行结果配合轮询的方式会造成cpu空转,主线程也会等待get()方法结束,显然这是不行的,而且代码实现起来也不优雅,因此java8引入了CompletableFuture,功能更为强大。
CompletableFuture登场
之前谈到Future提供的get()方法配合isDone的轮询方法获取异步多线程任务执行的结果会导致cpu空转,程序堵塞,在处理复杂任务的时候尤为明显,因此为了解决这些问题CompletableFuture出现,它提供了一种类似于观察者模式的机制,当线程执行完成之后将会通知监听的一方,这样就不需要像Future一样不断轮询判断任务是否执行完成了。
CompletableFuture架构简述
CompletableFuture类的架构说明:
- 接口CompletionStage:代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段。一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发。
- 类CompletableFuture:提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法。它可能代表一个明确完成的Future,也可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些函数或执行某些动作。
CompletableFuture四大静态方法创建异步任务
四个静态构造方法
对于上述的executor参数,如果不传的话,默认使用ForkJoinPoolcommonPool()线程,这个线程有时候会充当守护线程的角色,也就是当主线程结束的时候,它会随着JVM一起关闭,但是如果该线程执行的任务没有完成的话就会导致任务执行结果的丢失;如果传入一个线程池对象,就会使用这个线程池对象来创建线程执行异步任务。下面是一段演示代码:
public class CompletableFutureDemo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 8,
TimeUnit.SECONDS, new LinkedBlockingDeque<>(10),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
try {
CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "----come in");
int result = ThreadLocalRandom.current().nextInt(10);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(result>3)
result/=0;
return result;
},executor).whenComplete((v, e) -> {
if (e == null)
System.out.println("计算完成,结果是" + v);
}).exceptionally(e -> {
e.printStackTrace();
System.out.println("异常情况" + e.getMessage());
return null;
});
System.out.println(Thread.currentThread().getName()+"告辞");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
executor.shutdown();
}
}
}
这里我们使用supplyAsync(Supplier<U> supplier,Executor executor)静态方法获取异步任务执行的结果。如果任务执行的过程中没有异常,则走whenComplete()方法,反之走exceptionally()方法,下面是分别走两个方法的执行结果。
由此我们可以看出,CompletableFuture的优点如下,当异步任务结束或者出现异常时,会自动调用某个对象的回调方法,当主线程设置好回调之后,主线程就不用关注异步任务的执行结果,也不会被异步任务所影响,异步任务之间可以顺序执行。
多线程之CompletableFuture的总结
Future接口定义的方法在处理简单任务的时候足够应对,短暂时间的异步任务的执行在获取结果的时候并不会造成很严重的程序阻塞,但是复杂任务的执行容易造成cpu空转和程序的阻塞。CompletableFuture对Future进行了扩展,在Future定义的方法基础上,引入了类似观察者模式的机制,使得当任务执行完毕或者异常的时候能够调用某个对象的回调方法,通知任务的监控者获取数据或者捕获异常。这样,主线程和子线程之间互不干扰,提升了程序的运行效率。因此在执行相对简单的异步任务时,我们可以使用线程池配合FutureTask执行异步多线程任务,而当执行相对复杂(任务执行时间较长)的任务时,建议使用线程池配合CompletableFuture执行异步多线程任务。