CompletableFuture
文章目录
1. Future接口理论知识
- Future接口定义了异步任务执行的一些方法,如获取异步任务的结果、取消任务的执行、判断任务是否取消、判断任务是否完毕
- Future是异步任务的执行方法,让子线程去执行某些任务,然后主线程继续执行,过段时间后获取主线程的执行结果
- 通过在主线程中创建异步线程,让异步线程去执行操作,然后再调用方法获得结果
- FutureTask就是Future接口的实现类
- 异步任务可以获得执行结果,普通线程无法获得执行结果;可以在创建FutureTask时设置返回值类型,然后传入一个Callable接口实现类,重写call()时可以返回结果
2. FutureTask实现类异步任务
- 并发编程要满足 多线程、有返回、异步任务,故要同时满足Thread\Callable\Future,而创建Thread必须传入Runnable接口实现类(多线程),且使用返回值必须使用Callable接口实现类(有返回),实现异步任务必须是Future接口实现类(异步任务);故使用FutureTask,其是RunnableFuture接口的实现了,而且可以传入Callable实现类来创建对象
- FutureTask是RunnableFuture接口的实现类,其可以通过Callable接口实现类来创建对象,然后根据该对象创建的Thread线程就可以实现异步任务![[attachments/Pasted image 20241201224826.png]]
- FutureTask是Future接口和Runnable接口共同的实现类,用来实现异步任务并返回结果,且FutureTask实现类没有空参构造,只支持使用Callable实现类或者Runnable进行构造(因为实现了Runnable接口,必须重写run()),当创建好FutureTask对象后,此时就可以传入Thread中来创建线程,因为实现了Runnable接口,故可以直接传入来构建线程
- 创建线程Thread时可以通过Runnable接口实现类来创建同步线程,此时不可以返回结果;也可以通过Callable接口实现类先创建一个FutureTask对象,然后再创建线程,此时创建的是异步任务线程,可以获得返回值
- 创建线程Thread只可以传入Runnable实现类,如果要使用Callable来创建线程,则必须先使用Callable对象创建FutureTask类,然后作为对象传入Thread中
- FutureTask是RunnableFuture接口的实现类,既实现了Runnable接口来创建线程, 也实现了Future接口来实现异步任务,因为Runnable接口必须要重写方法,故没有无参构造,必须传入参数,要么传入Runnable接口实现类,要么传入Callable 接口实现类,当传入Callable 接口实现类时,此时就可以获得返回值
- Callable接口要重写call()方法,且必须指定返回值,并且call()会抛出异常
- 通过FutureTask创建线程后,此时使用 thread.start() 来启动线程并执行异步任务,然后使用 futureTask.get() 来获得异步任务的结果
3. Future优缺点
- Future可以实现异步任务;Runnable可以创建线程;Callable可以有返回值
- 使用FutureTask并用Callable实现类来创建,Callable接口实现时必须传入返回值类型,即泛型
- 直接使用FutureTask时每次都要创建一个实例对象来执行异步任务,且无法实现线程的复用,故效率和开销很大,此时可以使用线程池来复用线程
- 使用FutureTask和线程池异步多线程任务配合,此时只需要创建一个线程池来执行FutureTask的异步任务,而不用为每个FutureTask都创建一个Thread来执行
- FutureTask的get()容易造成阻塞,get()必须等待异步任务完成之后才可以得到返回结果
- 对于FutureTask任务,如果调用get()就会必须等待任务结束后才可以继续执行,会阻塞,故一般把get()放在最后再执行
- 也可以在调用get()时传入一个超时时间,当超过该时间没返回时,就不会再继续等待,而是抛出异常
- 且当调用get()时,CPU会一直轮询判断get()是否完成,会一直轮询判断get()是否完成,可以在调用get()时传入超时时间,此时当超过超时时间未完成时会抛出异常, 不会一直阻塞
- 可以通过while循环内isDone()来判断异步任务是否完成,但轮询会耗费无所谓的CPU资源
- FutureTask获得结果时会阻塞,可以设置超时时间,但会抛出异常,也可以通过isDone()来判断,但会一直while循环,但轮询会一直占用CPU,故可以使用回调通知
- 回调通知、创建异步任务、多个任务前后依赖可以组合处理、对计算速度选最快
4. CompleteableFuture对Future的改进
- 回调通知、创建异步任务、多个任务前后依赖可以组合处理、对计算速度选最快
- CompletableFuture为什么会出现
- FutureTask执行异步任务时,如果调用get()则会一直阻塞,如果使用isDone()轮询的方式则会一直占用CPU
- 而真正的异步任务是想通过回调的方式进行通知
- CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方,实现了回调通知,当任务完成后执行回调函数
- CompletableFuture源码分析
- 实现了Future接口和CompletionStage接口
- CompletionStage接口:代表异步任务过程中的某一个阶段,一个阶段完成之后可能会触发另外一个阶段,类似于Linux中的管道分隔符,一个阶段一个阶段的完成
- CompletableFuture 实现了Future的所有功能,并进行扩展,可能代表一个完成的Future,也可能代表一个完成阶段CompletionStage
- CompletableFuture实现了Future接口和CompletionStage接口
- 核心四个静态方法,来创建一个异步任务
- 通过静态方法来得到CompletableFuture对象
- runAsync无返回值,传入Runnable对象,可选Executor线程池
- supplyAsync有返回值,传入Supplier对象(Callable就是Supplier,有返回值),可选Executor线程池
- Executor线程池:如果没有指定Executor的方法,直接使用默认ForkJoinPool.commonPool()作为线程池执行异步代码,此时默认使用ForkJoinPool的线程池来运行异步任务
- 使用静态方法创建好CompletableFuture对象后,
- Executor线程池使用Executors工具类可以创建三种类型(单个、定长、可变)的线程池,但不推荐,因为内部的阻塞队列是MAX长的,此时一个自定义七个参数(核心线程个数、最大线程个数、阻塞队列、存活时间、线程工厂类、拒绝策略)来创建线程池对象Executor
- CompletableFuture回调通知,减少阻塞和轮询
- CompletableFuture是Future的功能增强版,减少了阻塞和轮询,其可以传入回调对象,当异步任务完成时,自动调用回调对象的回调方法
- 因为使用线程池来运行异步任务,故不使用start()就会自动运行,线程池是先请求核心线程,当没有核心线程时则进入阻塞队列,当阻塞队列满时看能不能新建线程,如果不可以则直接拒绝
- 创建 CompletableFuture 时,可以通过whenComplete((v,e)->{})来设置回调函数,此时当运行结束后就会调用设置的回调函数,此时CompletableFuture执行结束后会调用whenCompletable中传入的函数,且以(v,e)为参数,此时的v就是CompletableFuture返回结果,e为异常
- 也可以通过 .exceptionally(e->{}) 来对异常情况进行处理
- 当没有传入Executor线程池时,此时会使用默认的ForkJoinPool,该线程池是守护线程, 当主线程结束后则会自动结束,故此时其内运行的线程也会结束,没有指定线程池时会使用ForkJoinPool线程池,该线程池是守护线程
- CompletableFuture可以实现回调通知,通过内部的静态方法来创建对象,然后使用.whenComplate()来创建回调函数,此时当运行结束后会自动调用其内的函数,以(v,e)为参数,有返回值时,v就是返回的结果,然后可以使用 .exceptionally(e->{})来处理异常的情况
- completableFuture可以使用回调通知的方式来执行异步任务,通过静态方法来创建对象,可以指定线程池,不指定时使用默认的ForkJoinPool线程池,此时是守护线程,当主线程结束后就会立即结束,使用线程池时会自动执行,不需要手动的start()
- CompletableFuture代表了异步任务的某一个阶段,代表了某一个阶段或者某一个异步任务,当一个阶段完成后可能会执行下一个阶段,通过回调通知的方式返回结果,即通过.whenComplete((v,e)->{}),当执行结束且没有异常时就会执行传入的函数,此时就可以对结果进行处理或者开启下一个阶段的任务
- CompletableFuture异步任务结束之后会自动回调某个对象的方法,即回调通知;主线程设置好回调函数后就不关心异步任务的执行,异步任务可以顺序执行;异步任务出错时,也会自动调用某个对象的方法
5. CompletableFuture大厂案例精讲
- 函数式编程
- 函数式接口
- Runnable:无参数无返回值,run()函数
- Function:接收一个参数,有返回值,apply()函数
- Consumer:消费性函数接口,接收一个参数,但是没有返回值,accept()函数
- BiConsumer:接收两个参数,没有返回值,accept()函数
- Supplier:供给型函数接口,没有参数,有一个返回值,get()函数
- Chain链式调用
- 必须保证每个函数的返回值是自己时才可以使用链式调用
- 如 T.setA().setB();,必须保证 T.setA()的返回值是T本身才可以实现链式调用
- 函数式接口
- **join()和get()对比
- ForkJoin分支合并,可以将一个大的任务拆分为多个子任务分别计算,然后使用join将结果合并
- join()和get()都可以获得异步任务的返回结果,但get()必须做异常处理,而join()不需要做异常处理
- join()和get()都可以获得异步任务的结果,但get()必须进行异常处理,而join()不需要进行异常处理
- 需求说明
- 先完成功能再完善性能!
- 顺序执行的任务使用异步任务去完成,对比顺序执行和异步执行的性能
- 业务实现
- 使用流式编程,将集合通过.stream()转换为流对象,然后对每个对象通过.map()映射为另一个对象,再通过.collect()转换为一个集合,其中可以映射为异步任务的对象集合,再将其结果重新映射
- 使用流式编程,先.stream()转换为流,然后map(Function)通过函数式编程将流的每一个对象进行处理,映射为新的对象,然后使用 .collect(Collectors.toList()) 将映射的结果重新变为集合
- 流式编程就是将一个数据流映射到另一个数据流,如将一个List<>映射到另一个List<>,对其内的每一个元素进行处理映射到另一个元素上
- 在流式编程时也可以使用CompletableFuture对每个对象进行异步任务处理,此时会映射为 List< CompletableFuture<> >,然后再.stream()转换为流,继续对每个元素使用 join() 得到返回值对象
- 从功能到性能
6. CompleteableFuture常用方法
- 通过静态方法来创建CompletableFuture对象,有runAsync无返回值和supplyAsync有返回值两种,均可以传入Executor线程池,然后可以通过 .whenComplete(BiComsumer) 来处理执行完成的后续操作,也可以通过 .exectp…来处理异常
- CompletableFuture不仅实现了Future接口,还实现了CompletionStage接口,故不仅可以是一个完整的异步任务,也可以是任务的某个阶段
- 获得结果和触发计算
- 获得结果可以通过get()和join(),也可以在get()中设置超时时间,超时会抛出异常,还可以通过 getNow(T)
- get()方法必须进行异常处理,join()不需要异常处理,都可以获得异步任务的处理结果,且可以在get()时指定超时时间
- 也可以通过getNow()获得计算结果,此时要传入一个default缺省值,表示如果此时获得时没有完成,则返回设置的缺省值,服务降级,没有计算完成时返回一个设置的默认值,不会抛出异常,在使用get()时会一直阻塞,且如果设置超时时间则会抛出异常,故此时可以使用getNow(default),当执行时如果没有执行结束则会返回缺省值,且不会抛出异常
- 主动触发计算可以通过 complete() 函数返回boolean类型,complete(value)会打断get()阻塞,直接将value作为计算结果,就是说如果执行complete()时没有执行结束,则直接返回true,并将value作为计算结果,否则返回false,计算结果不变
- 对计算结果进行处理
- thenApply:计算结果存在依赖关系,将两个线程串行化,在第一个异步任务完成后,调用thenApply(Function)将上一步的结果作为下一步的输入继续执行,最后一步的返回值就是整个异步任务的返回值,因为前后步存在依赖关系,故如果当前步骤有异常则会叫停,当前步错不走下一步,前后存在依赖关系
- 此时会创建多个阶段来执行任务,最后一步任务的执行结果就是最后的返回结果
- handle:将两个线程串行化,正常情况和thenApply一样,将上一步的返回结果作为参数传入下一步执行,但是出错后仍会继续向下走,带着错误参数向下走,此时传入的不再仅仅是上一步的结果(f),而是会带着错误参数(f,e),handle有异常也可以向下走,带着异常参数进行处理,handle也是将上一步的结果作为当前步的参数输入,并返回结果,但其出现错误时仍会继续运行, 且将错误信息作为参数返回,故此时handle传入的是BiFunction
- 对计算结果进行消费
- 对计算结果进行处理使用thenApply或者handle,都存在依赖关系,将两个线程串行化,将上一步的结果作为下一步的参数进行传入并执行,一个出错后就不再执行,一个出错后将错误参数传入继续执行,故每一步都要返回一个结果,作为下一步的参数
- 对计算结果进行消费时,接收任务的处理结果,并消费处理,无返回结果,传入Consumer函数式接口,使用thenAccept时传入参数,但没有返回值
- 对计算结果消费时,不进行返回,只进行消费
- 通过thenAccept()来实现对计算结果的消费(Consumer函数式接口通过accept()函数实现)
- thenRun(Runnable)不需要上一步的结果也没有返回;thenApply(Function)需要上一步的结果,且有返回值;thenAccept(Consumer)需要上一步的结果但没有返回值
- 且如果最后调用join()或者get()得到返回结果时,此时返回的是最后一步的返回值,如果最后一步是thenRun或者thenAccept,则此时就没有返回结果
- CompletableFuture和线程池
- 对于同一个CompletableFuture异步任务中的不同阶段,如果不加线程池,则此时会使用同一个默认线程池中的线程串行执行
- 但如果第一步使用的自定义线程池,其后面没有Async时则会使用自定义线程池,但如果有Async则会创建默认线程池运行,但可能因为优化,使用Main线程;Async函数不可以再传入线程池
- 当前任务阶段使用和前一个任务阶段相同的线程,但如果当前任务使用thenXXXAsync异步执行,则此时就不再使用上一步的线程池
- CompletableFuture和线程池,同一个异步任务的不同任务阶段可能使用不同的线程,当前任务使用和上一个任务相同的线程,但如果当前任务使用Async,则会重新创建一个线程来执行,不会再使用上一个任务的线程
- 但有可能因为处理太快,系统优化切换原则,直接使用main处理,有可能处理太快由于系统优化原则,直接使用main处理
- 使用thenAsync且上一步使用自定义线程池时,此时就会创建一个默认的ForkJoinPool来执行当前任务,会创建一个默认线程池,不会使用上一步的自定义线程池
- 使用Async函数创建一个新任务时,此时就会创建一个ForkJoinPool来执行当前任务
- 对计算速度进行选用
- 谁快用谁,对于多个异步任务,谁快就返回谁的结果
- A.applyToEither(B,BiFunction)来实现谁快就返回谁,其中A和B是使用CompletableFuture创建的异步任务,Function是函数式接口,传入获胜者的计算结果,对其进行处理,并返回一个结果;且该函数也会返回一个CompletableFuture对象,并将Function的返回值作为返回结果,会等待二者执行完成后再判读
- 调用A.applyToEither(B,Function)时,会等待A和B均执行完毕后才会比较谁更快,会等待两个异步任务均执行完成后才会执行,并返回一个异步任务
- 对计算结果进行合并
- 两个CompletableStage任务都完成后,最终把两个任务的结果一起交给thenCombine来处理,即将两个异步任务的结果合并处理,前提是两个任务均执行完成,故先完成的等待另一个完成
- 先完成的等待其他分支任务
- A.thenCombine(B, BiFunction),将两个任务的结果传入BiFunction进行处理,并返回一个结果,且该函数也返回一个CompletableFuture对象,以thenCombine返回的结果作为该对象的计算结果返回**
- 必须等待两个任务均执行完成后才可以合并
- 总结
- 使用CompletableFuture的静态方法先创建一个任务,其可以是一个完整的Future任务,也可以是Future的一个阶段,使用thenXxx来串行执行后面的依赖任务,可以在其后面串行执行别的任务阶段,通过thenXxx来执行
- .runAsync是无返回值的异步任务,传入Runnable实现类对象,.supplyAsync是有返回值的异步任务,传入Callable实现类对象