一、Callable&Future&FutureTask介绍
Java项目编程中,为了充分利用计算机CPU资源,一般开启多个线程来执行异步任务,但是不论继承Thread还是实现Runnable接口都无法获取任务执行的结果,因此java1.5就提供了Callable接口来实现这一场景,而Future和FutureTask就可以和Callable接口配合起来使用。
1-1、Callable和Runnable的区别
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Runnable 的缺陷:
- 不能返回一个返回值
- 不能抛出 checked Exception
Callable的call方法可以有返回值,可以声明抛出异常。和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 强大。
@Slf4j
public class FutureDemo {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
log.info("普通线程执行");
}
}).start();
new Thread(()->{
log.info("Lambda方式线程执行");
}).start();
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
System.out.println("通过Callable方式执行任务");
Thread.sleep(3000);
return "返回任务结果";
}
});
log.info("a:"+task.isDone());
new Thread(task).start();
log.info("b:"+task.isDone());
log.info("执行结果:{}",task.get());
log.info("c:"+task.isDone());
}
}
执行结果:
通过上图可以看到通过使用FutureTask,然后传入Callable,就可以通过
get()获得线程执行的结果,通过isDone()就可以获得线程是否执行完毕。
1-2、Future 的主要功能
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
- boolean cancel (boolean mayInterruptIfRunning) 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
- boolean isCancelled () 任务是否已经取消,任务正常完成前将其取消,则返回 true
- boolean isDone () 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true
- V get () throws InterruptedException, ExecutionException 等待任务执行结束,然后获得V类型的结果。InterruptedException 线程被中断异常, ExecutionException任务执行异常,如果任务被取消,还会抛出CancellationException
- V get (long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException 同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException
通过以上的了解,我们就可以试着用FutureTask做一个多线程并行的操作,比如我们要泡茶,如果没有多线程的方式,
1、我们使用单线程的方式泡茶顺序如下:
洗杯子->放茶叶->烧水->倒水
2、使用多线程的方式泡茶:
烧水
洗杯子->放茶叶->获得水是否烧开(烧开)->倒水
通过下面多线程的方式,我们就可以在烧水的时候去洗杯子,放茶叶,这样就可以让多个任务并行操作。
1-2-1、根据FutureTask原理实现自己的FutureTask
通过上面FutureTask的实现过程,可以看出来get是一个阻塞的获取结果方法,当有结果的时候即可以返回执行的结果,根据这个机制,我们自己写一个FutureTask
@Slf4j
public class MyFutureTask<V> implements Runnable, Future {
private Callable<V> callable;
private V result = null;
public MyFutureTask(Callable<V> callable) {
this.callable = callable;
}
@Override
public void run() {
try {
result=callable.call();
synchronized (this){
this.notify();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return this.result!=null;
}
@Override
public Object get() throws InterruptedException, ExecutionException {
if(result!=null){
return result;
}else{
synchronized (this){
this.wait();
}
}
return result;
}
@Override
public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
if(result!=null){
return result;
}else {
if(timeout>0L){
unit.sleep(timeout);
if (result!=null){
return result;
}else{
throw new TimeoutException();
}
}
return result;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyFutureTask task = new MyFutureTask(new Callable() {
@Override
public Object call() throws Exception {
Thread.sleep(3000);
return "返回任务结果";
}
});
new Thread(task).start();
log.debug("结果:{}",task.get());
}
}
执行结果:
1-3、利用 FutureTask 创建 Future
Future实际采用FutureTask实现,该对象相当于是消费者和生产者的桥梁,消费者通过 FutureTask 存储任务的处理结果,更新任务的状态:未开始、正在处理、已完成等。而生产者拿到的 FutureTask 被转型为 Future 接口,可以阻塞式获取任务的处理结果,非阻塞式获取任务处理状态。
FutureTask既可以被当做Runnable来执行,也可以被当做Future来获取Callable的返回结果
1-3-1、使用案例:促销活动中商品信息查询
在维护促销活动时需要查询商品信息(包括商品基本信息、商品价格、商品库存、商品图片、商品销售状态等)。这些信息分布在不同的业务中心,由不同的系统提供服务。如果采用同步方式,假设一个接口需要50ms,那么一个商品查询下来就需要200ms-300ms,这对于我们来说是不满意的。如果使用Future改造则需要的就是最长耗时服务的接口,也就是50ms左右。
public class FutureTaskDemo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> ft1 = new FutureTask<>(new T1Task());
FutureTask<String> ft2 = new FutureTask<>(new T2Task());
FutureTask<String> ft3 = new FutureTask<>(new T3Task());
FutureTask<String> ft4 = new FutureTask<>(new T4Task());
FutureTask<String> ft5 = new FutureTask<>(new T5Task());
//构建线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(ft1);
executorService.submit(ft2);
executorService.submit(ft3);
executorService.submit(ft4);
executorService.submit(ft5);
//获取执行结果
System.out.println(ft1.get());
System.out.println(ft2.get());
System.out.println(ft3.get());
System.out.println(ft4.get());
System.out.println(ft5.get());
executorService.shutdown();
}
static class T1Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T1:查询商品基本信息...");
TimeUnit.MILLISECONDS.sleep(5000);
return "商品基本信息查询成功";
}
}
static class T2Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T2:查询商品价格...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品价格查询成功";
}
}
static class T3Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T3:查询商品库存...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品库存查询成功";
}
}
static class T4Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T4:查询商品图片...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品图片查询成功";
}
}
static class T5Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T5:查询商品销售状态...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品销售状态查询成功";
}
}
}
执行结果:
给任务1设置2秒超时时间,再次执行结果如下:
通过以上我们总结出来:虽然FutureTask能让多个任务同时执行,但是缺点是如果5个任务同时执行,第一个任务要等待很久,那任务就会卡在第一个任务,导致结果无法返回,这在一些实际场景中是不允许的。我们可以设置一个超时时间,这样就不需要一直等待下去。
而有些业务场景即使其中某一个某几个任务无法返回,那其他任务也需要即时的返回执行结果,这样就可以使用 CompletionService 这个后面再进行使用
1-4、Future 注意事项
当 for 循环批量获取 Future 的结果时容易 block,get 方法调用时应使用 timeout 限制Future 的生命周期不能后退。一旦完成了任务,它就永久停在了“已完成”的状态,不能从头再来
1-5、Future的局限性
从本质上说,Future表示一个异步计算的结果。它提供了isDone()来检测计算是否已经完成,并且在计算结束后,可以通过get()方法来获取计算结果。在异步计算中,Future确实是个非常优秀的接口。但是,它的本身也确实存在着许多限制:
并发执行多任务:Future只提供了get()方法来获取结果,并且是阻塞的。所以,除了等待你别无他法;无法对多个任务进行链式调用:如果你希望在计算任务完成后执行特定动作,比如发邮件,但Future却没有提供这样的能力;无法组合多个任务:如果你运行了10个任务,并期望在它们全部执行结束后执行特定动作,那么在Future中这是无能为力的;没有异常处理:Future接口中没有关于异常处理的方法;
二、CompletionService
上面提到如果多个任务,有个别任务无法获得执行结果,那么就需要将已经获得执行结果的值进行返回,FutureTask无法做到这一点,CompletionService就可以实现上面的需求
Callable+Future 可以实现多个task并行执行,但是如果遇到前面的task执行较慢时需要阻塞等待前面的task执行完后面task才能取得结果。而CompletionService的主要功能就是一边生成任务,一边获取任务的返回值。让两件事分开执行,任务之间不会互相阻塞,可以实现先执行完的先取结果,不再依赖任务顺序了。
2-1、CompletionService原理
内部通过阻塞队列+FutureTask,实现了任务先完成可优先获取到,即结果按照完成先后顺序排序,内部有一个先进先出的阻塞队列,用于保存已经执行完成的Future,通过调用它的take方法或poll方法可以获取到一个已经执行完成的Future,进而通过调用Future接口实现类的get方法获取最终的结果
2-2、询价示例
下面以一个询价的示例来进行讲述,哪个询价结果先返回,就先进行保存哪个
@Slf4j
public class CompletionServiceDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
//创建CompletionService
CompletionService<Integer> cs = new ExecutorCompletionService<>(executor);
//异步向电商S1询价
cs.submit(() -> getPriceByS1());
//异步向电商S2询价
cs.submit(() -> getPriceByS2());
//异步向电商S3询价
cs.submit(() -> getPriceByS3());
//将询价结果异步保存到数据库
for (int i = 0; i < 3; i++) {
//从阻塞队列获取futureTask
Integer r = cs.take().get();
executor.execute(() -> save(r));
}
executor.shutdown();
}
private static void save(Integer r) {
log.debug("保存询价结果:{}",r);
}
private static Integer getPriceByS1() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(5000);
log.debug("电商S1询价信息1200");
return 1200;
}
private static Integer getPriceByS2() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(8000);
log.debug("电商S2询价信息1000");
return 1000;
}
private static Integer getPriceByS3() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(3000);
log.debug("电商S3询价信息800");
return 800;
}
}
执行结果:
2-3、应用场景总结
当需要批量提交异步任务的时候建议你使用CompletionService。CompletionService将线程池Executor和阻塞队列BlockingQueue的功能融合在了一起,能够让批量异步任务的管理更简单。CompletionService能够让异步任务的执行结果有序化。先执行完的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如Forking Cluster这样的需求。线程池隔离。CompletionService支持自己创建线程池,这种隔离性能避免几个特别耗时的任务拖垮整个应用的风险