直接继承Thread或者实现Runnable接口都可以创建线程,但是这两种方法都有一个问题就是:没有返回值,也就是不能获取执行完的结果。因此java1.5就提供了Callable接口来实现这一场景,而Future和FutureTask就可以和Callable接口配合起来使用。
思考:为什么需要 Callable?
@FunctionalInterface
public interface Runnable {public abstract void run();
}
@FunctionalInterface
public interface Callable {V call() throws Exception;
Runnable 的缺陷:
Callable的call方法可以有返回值,可以声明抛出异常。和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况
,或者取消任务的执行
,还可获取任务执行的结果
,这些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 强大。
new Thread(new Runnable() {@Overridepublic void run() {System.out.println("通过Runnable方式执行任务");}
}).start();FutureTask task = new FutureTask(new Callable() {@Overridepublic Object call() throws Exception {System.out.println("通过Callable方式执行任务");Thread.sleep(3000);return "返回任务结果";}
});
new Thread(task).start();
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
boolean cancel (boolean mayInterruptIfRunning)
boolean isCancelled ()
boolean isDone ()
V get () throws InterruptedException, ExecutionException
// InterruptedException 线程被中断异常, ExecutionException任务执行异常,如果任务被取消,还会抛出CancellationException
V get (long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
Future实际采用FutureTask实现,该对象相当于是消费者和生产者的桥梁,消费者通过 FutureTask 存储任务的处理结果,更新任务的状态:未开始、正在处理、已完成等。而生产者拿到的 FutureTask 被转型为 Future 接口,可以阻塞式获取任务的处理结果,非阻塞式获取任务处理状态。
FutureTask既可以被当做Runnable来执行,也可以被当做Future来获取Callable的返回结果。
把 Callable 实例当作 FutureTask 构造函数的参数,生成 FutureTask 的对象,然后把这个对象当作一个 Runnable 对象,放到线程池中或另起线程去执行,最后还可以通过 FutureTask 获取任务执行的结果。
public class FutureTaskDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {Task task = new Task();//构建futureTaskFutureTask futureTask = new FutureTask<>(task);//作为Runnable入参new Thread(futureTask).start();System.out.println("task运行结果:"+futureTask.get());}static class Task implements Callable {@Overridepublic Integer call() throws Exception {System.out.println("子线程正在计算");int sum = 0;for (int i = 0; i < 100; i++) {sum += i;}return sum;}}
在维护促销活动时需要查询商品信息(包括商品基本信息、商品价格、商品库存、商品图片、商品销售状态等)。这些信息分布在不同的业务中心,由不同的系统提供服务。如果采用同步方式,假设一个接口需要50ms,那么一个商品查询下来就需要200ms-300ms,这对于我们来说是不满意的。如果使用Future改造则需要的就是最长耗时服务的接口,也就是50ms左右。
public class FutureTaskDemo2 {public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask ft1 = new FutureTask<>(new T1Task());FutureTask ft2 = new FutureTask<>(new T2Task());FutureTask ft3 = new FutureTask<>(new T3Task());FutureTask ft4 = new FutureTask<>(new T4Task());FutureTask 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 {@Overridepublic String call() throws Exception {System.out.println("T1:查询商品基本信息...");TimeUnit.MILLISECONDS.sleep(50);return "商品基本信息查询成功";}}static class T2Task implements Callable {@Overridepublic String call() throws Exception {System.out.println("T2:查询商品价格...");TimeUnit.MILLISECONDS.sleep(50);return "商品价格查询成功";}}static class T3Task implements Callable {@Overridepublic String call() throws Exception {System.out.println("T3:查询商品库存...");TimeUnit.MILLISECONDS.sleep(50);return "商品库存查询成功";}}static class T4Task implements Callable {@Overridepublic String call() throws Exception {System.out.println("T4:查询商品图片...");TimeUnit.MILLISECONDS.sleep(50);return "商品图片查询成功";}}static class T5Task implements Callable {@Overridepublic String call() throws Exception {System.out.println("T5:查询商品销售状态...");TimeUnit.MILLISECONDS.sleep(50);return "商品销售状态查询成功";}}
Future 注意事项
思考: 使用Callable 和Future 产生新的线程了吗?
从本质上说,Future表示一个异步计算的结果。它提供了isDone()来检测计算是否已经完成,并且在计算结束后,可以通过get()方法来获取计算结果。在异步计算中,Future确实是个非常优秀的接口。但是,它的本身也确实存在着许多限制:
并发执行多任务
:Future只提供了get()方法来获取结果,并且是阻塞的。所以,除了等待你别无他法;
无法对多个任务进行链式调用
:如果你希望在计算任务完成后执行特定动作,比如发邮件,但Future却没有提供这样的能力;
无法组合多个任务
:如果你运行了10个任务,并期望在它们全部执行结束后执行特定动作,那么在Future中这是无能为力的;
没有异常处理
:Future接口中没有关于异常处理的方法;
Callable+Future 可以实现多个task并行执行,但是如果遇到前面的task执行较慢时需要阻塞等待前面的task执行完后面task才能取得结果。而CompletionService的主要功能就是一边生成任务,一边获取任务的返回值。让两件事分开执行,任务之间不会互相阻塞,可以实现先执行完的先取结果,不再依赖任务顺序了。
内部通过阻塞队列+FutureTask,实现了任务先完成可优先获取到,即结果按照完成先后顺序排序, 内部有一个先进先出的阻塞队列,用于保存已经执行完成的Future
,通过调用它的take方法或poll方法可以获取到一个已经执行完成的Future,进而通过调用Future接口实现类的get方法获取最终的结果
采用“ThreadPoolExecutor+Future”的方案:异步执行询价然后再保存
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 异步向电商S1询价
Future f1 = executor.submit(()->getPriceByS1());
// 异步向电商S2询价
Future f2= executor.submit(()->getPriceByS2());
// 获取电商S1报价并异步保存
executor.execute(()->save(f1.get()));
// 获取电商S2报价并异步保存
executor.execute(()->save(f2.get())
如果获取电商S1报价的耗时很长,那么即便获取电商S2报价的耗时很短,也无法让保存S2报价的操作先执行,因为这个主线程都阻塞 在了f1.get()操作上。
使用CompletionService实现先获取的报价先保存到数据库
@Slf4j
public class CompletionServiceDemo {public static void main(String[] args) throws InterruptedException, ExecutionException {//创建线程池ExecutorService executor = Executors.newFixedThreadPool(10);//创建CompletionServiceCompletionService cs = new ExecutorCompletionService<>(executor);//异步向电商S1询价cs.submit(CompletionServiceDemo::getPriceByS1);//异步向电商S2询价cs.submit(CompletionServiceDemo::getPriceByS2);//异步向电商S3询价cs.submit(CompletionServiceDemo::getPriceByS3);//将询价结果异步保存到数据库for (int i = 0; i < 3; i++) {//从阻塞队列获取futureTaskInteger 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;}
}
Dubbo 中有一种叫做 Forking 的集群模式,这种集群模式下,支持并行地调用多个服务实例,只要有一个成功就返回结果。
geocoder(addr) {//并行执行以下3个查询服务, r1=geocoderByS1(addr);r2=geocoderByS2(addr);r3=geocoderByS3(addr);//只要r1,r2,r3有一个返回//则返回return r1|r2|r3;
代码模拟实现
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 创建CompletionService
CompletionService cs = new ExecutorCompletionService<>(executor);
// 用于保存Future对象
List> futures = new ArrayList<>(3);
//提交异步任务,并保存future到futures
futures.add(cs.submit(()->geocoderByS1()));
futures.add(cs.submit(()->geocoderByS2()));
futures.add(cs.submit(()->geocoderByS3()));
// 获取最快返回的任务执行结果
Integer r = 0;
try {// 只要有一个成功返回,则breakfor (int i = 0; i < 3; ++i) {r = cs.take().get();//简单地通过判空来检查是否成功返回if (r != null) {break;}}
} finally {//取消所有任务for(Future f : futures)f.cancel(true);
}
// 返回结果
当需要批量提交异步任务的时候建议你使用CompletionService
CompletionService将线程池Executor和阻塞队列BlockingQueue的功能融合在了一起,能够让批量异步任务的管理更简单。
CompletionService能够让异步任务的执行结果 「有序化」 先执行完的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如Forking Cluster这样的需求。
线程池隔离。CompletionService支持自己创建线程池,这种隔离性能避免几个特别耗时的任务拖垮整个应用的风险。
简单的任务,用Future获取结果还好,但我们并行提交的多个异步任务,往往并不是独立的,很多时候业务逻辑处理存在串行[依赖]、并行、聚合的关系。如果要我们手动用 Fueture 实现,是非常麻烦的。
CompletableFuture是Future接口的扩展和增强
。CompletableFuture实现了Future接口,并在此基础上进行了丰富地扩展,完美地弥补了Future上述的种种问题。
更为重要的是,CompletableFuture实现了对任务的 「编排」能力。借助这项能力,我们可以轻松地组织不同任务的运行顺序、规则以及方式。从某种程度上说,这项能力是它的核心能力。而在以往,虽然通过CountDownLatch等工具类也可以实现任务的编排,但需要复杂的逻辑处理,不仅耗费精力且难以维护。
jdk8 API文档
CompletionStage接口: 执行某一个阶段,可向下执行后续阶段。异步执行,默认线程池是ForkJoinPool.commonPool()
描述依赖关系
thenApply()
把前面异步任务的结果,交给后面的Function
thenCompose()
用来连接两个有依赖关系的任务,结果由第二个任务返回
描述and聚合关系
thenCombine
: 任务合并,有返回值
thenAccepetBoth
: 两个任务执行完成后,将结果交给thenAccepetBoth消耗,无返回值
runAfterBoth
: 两个任务都执行完成后,执行下一步操作(Runnable)
描述or聚合关系
applyToEither
: 两个任务谁执行的快,就使用那一个结果,有返回值。
acceptEither
: 两个任务谁执行的快,就消耗那一个结果,无返回值。
runAfterEither
: 任意一个任务执行完成,进行下一步操作(Runnable)。
并行执行
CompletableFuture类自己也提供了 anyOf()
和 allOf()
用于支持多个CompletableFuture并行执行
CompletableFuture 提供了四个静态方法来创建一个异步操作:
public static CompletableFuture runAsync(Runnable runnable)
public static CompletableFuture runAsync(Runnable runnable, Executor executor)public static CompletableFuture supplyAsync(Supplier supplier)
public static CompletableFuture supplyAsync(Supplier supplier, Executor executor)
这四个方法区别在于:
runAsync 方法以Runnable函数式接口类型为参数,没有返回结果
supplyAsync 方法Supplier函数式接口类型为参数,返回结果类型为U;Supplier 接口的 get() 方法是有返回值的(会阻塞)
没有指定Executor的方法会使用 ForkJoinPool.commonPool()
作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。
默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数)。如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议你要根据不同的业务类型创建不同的线程池,以避免互相干扰
Runnable runnable = () -> System.out.println("执行无返回结果的异步任务");
CompletableFuture.runAsync(runnable);
CompletableFuture future = CompletableFuture.supplyAsync(() -> {System.out.println("执行有返回值的异步任务");try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}return "Hello World";
});
String result = future.get();
join()和get()方法都是用来获取CompletableFuture异步之后的返回值。
当CompletableFuture的计算结果 「完成」,或者抛出异常的时候,我们可以执行特定的 Action。主要是下面的方法:
public CompletableFuture whenComplete(BiConsumer super T,? super Throwable> action)public CompletableFuture whenCompleteAsync(BiConsumer super T,? super Throwable> action)public CompletableFuture whenCompleteAsync(BiConsumer super T,? super Throwable> action, Executor executor)
Action的类型是BiConsumer super T,? super Throwable>,它可以处理正常的计算结果,或者异常情况。
方法不以 Async
结尾,意味着Action使用相同的线程执行,而Async可能会使用其它的线程去执行(如果使用相同的线程池,也可能会被同一个线程选中执行)。
这几个方法都会返回CompletableFuture,当Action执行完毕后它的结果返回原始的CompletableFuture的计算结果或者返回异常
这里代码可以优化使其更加整洁,但本着学习的心态我这边就不做输出了
CompletableFuture future = CompletableFuture.supplyAsync(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}if (new Random().nextInt(10) % 2 == 0) {int i = 12 / 0;}System.out.println("执行结束!");return "test";
});future.whenComplete(new BiConsumer() {@Overridepublic void accept(String t, Throwable action) {System.out.println(t+" 执行完成!");}
});future.exceptionally(new Function() {@Overridepublic String apply(Throwable t) {System.out.println("执行失败:" + t.getMessage());return "异常xxxx";}执行结束!
test 执行完成!
或者
执行失败:java.lang.ArithmeticException: / by zero
null 执行完成!
所谓结果转换,就是将上一段任务的执行结果作为下一阶段任务的入参参与重新计算,产生新的结果。
public CompletableFuture thenApply(Function super T,? extends U> fn)
public CompletableFuture thenApplyAsync(Function super T,? extends U> fn)
thenApply 接收一个函数作为参数,使用该函数处理上一个CompletableFuture 调用的结果,并返回一个具有处理结果的Future对象。
CompletableFuture future = CompletableFuture.supplyAsync(() -> {int result = 100;System.out.println("一阶段:" + result);return result;
}).thenApply(number -> {int result = number * 3;System.out.println("二阶段:" + result);return result;
});一阶段:100
二阶段:300
最终结果:300
在下面的示例中,我们使用thenCompose方法按顺序链接两个Future。
请注意,此方法 接受一个返回CompletableFuture实例的函数
。此函数的参数是上一计算步骤的结果。这允许我们在下一个CompletableFuture的lambda中使用此值:
public CompletableFuture thenCompose(Function super T, ? extends CompletionStage> fn);
public CompletableFuture thenComposeAsync(Function super T, ? extends CompletionStage> fn) ;
CompletableFuture future = CompletableFuture.supplyAsync(new Supplier() {@Overridepublic Integer get() {int number = new Random().nextInt(30);System.out.println("第一阶段:" + number);return number;}
}).thenCompose((result) -> CompletableFuture.supplyAsync(() -> {int num = result * 2;System.out.println("第二阶段:" + num);return num;
}));
第一阶段:10
第二阶段:20
最终结果: 20
两个方法都接收一个函数并将其应用于计算结果,但是thencomose(flatMap)方法接收一个返回另一个相同类型对象的函数。这种功能结构允许将这些类的实例组合为构建块。
如果我们想执行两个独立的未来,并对它们的结果进行处理,我们可以使用thenCombine方法,该方法接受一个未来和一个具有两个参数的函数来处理这两个结果
在前面的部分中,我们展示了有关thenApply()和thenCompose()的示例。两个api都有助于链接不同的CompletableFuture调用,但这两个函数的用法不同。
thenApply()
我们可以使用此方法处理上一次调用的结果。但是,需要记住的一点是,返回类型将由所有调用组合而成。
因此,当我们要转换CompletableFuture调用的结果时,此方法非常有用:
CompletableFuture finalResult = compute().thenApply(s-> s + 1);
thenCompose()
thenCompose()方法与thenApply()类似,因为两者都返回一个新的完成阶段。但是,thencose()使用前一阶段作为参数。它将展平并直接返回一个带有结果的CompletableFuture,而不是我们在thenApply()中观察到的嵌套CompletableFuture:
CompletableFuture result2 = future.thenCompose(param -> CompletableFuture.supplyAsync(() -> param + " World"));
因此,如果要链接可完成的CompletableFuture方法,那么最好使用thenCompose()。
与结果处理和结果转换系列函数返回一个新的 CompletableFuture 不同,结果消费系列函数只对结果执行Action,而不返回新的计算值。
根据对结果的处理方式,结果消费函数又分为:
对单个结果进行消费
public CompletableFuture thenAccept(Consumer super T> action)
不关心结果,只对结果执行Action
public CompletableFuture thenRun(Runnable action)
通过观察该系列函数的参数类型可知,它们是函数式接口Consumer,这个接口只有输入,没有返回值。
public CompletionStage thenAccept(Consumer super T> action);
public CompletionStage thenAcceptAsync(Consumer super T> action);CompletableFuture future = CompletableFuture.supplyAsync(() -> {int number = new Random().nextInt(10);System.out.println("第一阶段:" + number);return number;}).thenAccept(number ->System.out.println("第二阶段:" + number * 5));第一阶段:8
第二阶段:40
最终结果:null
对两个结果进行消费
thenAcceptBoth 函数的作用是,当两个 CompletionStage 都正常完成计算的时候,就会执行提供的action消费两个异步的结果。
public CompletionStage thenAcceptBoth(CompletionStage extends U> other,BiConsumer super T, ? super U> action);public CompletionStage thenAcceptBothAsync(CompletionStage extends U> other,BiConsumer super T, ? super U> action);
CompletableFuture.supplyAsync(() -> {int number = new Random().nextInt(3) + 1;System.out.println("第一阶段:" + number);try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}return number;}).thenAcceptBoth(CompletableFuture.supplyAsync(() -> {int number = new Random().nextInt(3) + 1;System.out.println("第二阶段:" + number);try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}return number;}), (n1, n2) -> {int result = n1 + n2;System.out.println("最终结果:" + result);});TimeUnit.SECONDS.sleep(3);第一阶段:2
第二阶段:1
最终结果:3
不关心结果,只对结果执行Action
public CompletionStage thenRun(Runnable action);
public CompletionStage thenRunAsync(Runnable action);
thenRun 也是对线程任务结果的一种消费函数,与thenAccept不同的是,thenRun 会在上一阶段 CompletableFuture 计算完成的时候执行一个Runnable,Runnable并不使用该 CompletableFuture 计算的结果。
CompletableFuture.supplyAsync(() -> {int number = new Random().nextInt(3) + 1;System.out.println("第一阶段:" + number);try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}return number;}).thenRun(() -> System.out.println("执行成功记录业务日志"));TimeUnit.SECONDS.sleep(3);第一阶段:3
执行成功记录业务日志
thenCombine 方法,合并两个线程任务的结果,并进一步处理。
public CompletionStage thenCombine(CompletionStage extends U> other,BiFunction super T,? super U,? extends V> fn);
public CompletionStage thenCombineAsync(CompletionStage extends U> other,BiFunction super T,? super U,? extends V> fn);
ExecutorService executorService = Executors.newFixedThreadPool(10);log.info("monkey进入餐厅,点了份西红柿炒番茄");
CompletableFuture cf = CompletableFuture.supplyAsync(()->{log.info("厨师炒菜");try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}return "西红柿炒番茄好了";
},executorService).thenCombine(CompletableFuture.supplyAsync(()->{log.info("服务员蒸饭");try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}return "米饭好了";
}),(dish,rice)->{log.info("服务员打饭");try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}return dish+","+rice;
});log.info(cf.get());
}02:08:10.543 [main] INFO com.tuling.future.CompletionServiceDemo - monkey进入餐厅,点了份西红柿炒番茄
02:08:10.584 [pool-1-thread-1] INFO com.tuling.future.CompletionServiceDemo - 厨师炒菜
02:08:10.585 [ForkJoinPool.commonPool-worker-9] INFO com.tuling.future.CompletionServiceDemo - 服务员蒸饭02:08:13.585 [ForkJoinPool.commonPool-worker-9] INFO com.tuling.future.CompletionServiceDemo - 服务员打饭
02:08:16.588 [main] INFO com.tuling.future.CompletionServiceDemo - 西红柿炒番茄好了,米饭好了
所谓线程交互,是指将两个线程任务获取结果的速度相比较,按一定的规则进行下一步处理。
两个线程任务相比较,先获得执行结果的,就对该结果进行下一步的转化操作。
public CompletionStage applyToEither(CompletionStage extends T> other,Function super T, U> fn);
public CompletionStage applyToEitherAsync(CompletionStage extends T> other,Function super T, U> fn);
CompletableFuture cf = CompletableFuture.supplyAsync(() -> {log.info("301路公交正在赶来");try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}return "301路到了";
}).applyToEither(CompletableFuture.supplyAsync(() -> {log.info("918路公交正在赶来");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}return "918路到了";
}), bus -> {if (bus.startsWith("918")) {throw new RuntimeException("918撞树了.......");}return bus;
}).exceptionally(e -> {log.error(e.getMessage());log.info("不做公交车了,叫出租车回家");try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException interruptedException) {interruptedException.printStackTrace();}return "出租车到了";
});log.info("{},坐出租车回家", cf.join());02:16:13.044 [ForkJoinPool.commonPool-worker-9] INFO com.tuling.future.CompletionServiceDemo - 301路公交正在赶来
02:16:13.044 [ForkJoinPool.commonPool-worker-2] INFO com.tuling.future.CompletionServiceDemo - 918路公交正在赶来
02:16:14.051 [ForkJoinPool.commonPool-worker-2] ERROR com.tuling.future.CompletionServiceDemo - java.lang.RuntimeException: 918撞树了.......
02:16:14.051 [ForkJoinPool.commonPool-worker-2] INFO com.tuling.future.CompletionServiceDemo - 不做公交车了,叫出租车回家
02:16:17.053 [main] INFO com.tuling.future.CompletionServiceDemo - 出租车到了,坐出租车回家
下一篇:ChatGPT-4震撼发布