面试题答案
一键面试CompletableFuture的supplyAsync与runAsync对比
- 性能
- supplyAsync:适用于有返回值的异步任务。由于要处理返回值,在任务执行结束后,需要额外的操作来包装返回值,相比
runAsync
可能会有稍高的开销。例如,在一个电商系统中,异步查询商品库存数量,supplyAsync
可以返回库存数量供后续业务逻辑使用。 - runAsync:用于无返回值的异步任务,没有返回值处理的额外开销,在单纯执行一些不需要返回结果的任务(如日志记录、缓存更新等)时,性能可能略优于
supplyAsync
。例如,在用户下单后,异步记录下单日志,使用runAsync
就足够。
- supplyAsync:适用于有返回值的异步任务。由于要处理返回值,在任务执行结束后,需要额外的操作来包装返回值,相比
- 线程资源占用
- supplyAsync:和
runAsync
默认都使用ForkJoinPool.commonPool()
线程池来执行任务。如果任务执行时间较长,会占用线程资源,影响线程池中其他任务的执行。例如,在大数据处理场景中,一个异步数据清洗任务如果执行时间长达几分钟,会占用commonPool
中的线程,导致其他短任务等待。 - runAsync:同样占用线程资源,但由于不涉及返回值处理,在内存占用上相对
supplyAsync
可能会少一些,因为不需要为返回值分配额外的内存空间。
- supplyAsync:和
- 任务调度策略
- supplyAsync:将任务提交到线程池后,线程池根据其调度策略(如FIFO等)来安排任务执行。执行完成后,会将返回值包装到
CompletableFuture
对象中。 - runAsync:任务提交到线程池后,按线程池调度策略执行,执行结束后,
CompletableFuture
仅表示任务已完成,无返回值。
- supplyAsync:将任务提交到线程池后,线程池根据其调度策略(如FIFO等)来安排任务执行。执行完成后,会将返回值包装到
根据不同需求选择方法
- 侧重响应时间
- 如果希望尽快得到任务执行结果,对于有返回值的任务优先选择
supplyAsync
。例如,在实时交易系统中,查询账户余额的异步任务,需要尽快得到余额值,使用supplyAsync
能及时返回结果。同时,可以配置一个独立的线程池,设置合适的线程数,避免commonPool
中其他任务的干扰。比如,根据服务器CPU核心数,设置线程池大小为CPU核心数 * 2
,使任务能更快执行。 - 对于无返回值且希望快速执行完的任务,使用
runAsync
。比如在支付成功后的即时通知发送任务,不需要返回值,用runAsync
并配置独立线程池,加快任务执行,提高响应时间。
- 如果希望尽快得到任务执行结果,对于有返回值的任务优先选择
- 侧重吞吐量
- 对于大量有返回值的任务,合理配置线程池并使用
supplyAsync
。例如在订单处理系统中,同时处理多个订单的商品信息查询任务,通过增大线程池大小(但不能过大,避免资源耗尽),充分利用系统资源,提高单位时间内处理任务的数量,从而提高吞吐量。 - 对于无返回值任务,
runAsync
配合优化的线程池同样可以提高吞吐量。例如在日志收集系统中,大量的日志记录任务,使用runAsync
并合理设置线程池参数,如队列大小等,能提高系统处理日志记录的能力。
- 对于大量有返回值的任务,合理配置线程池并使用
- 侧重资源利用率
- 当系统资源有限时,无论是
supplyAsync
还是runAsync
,都要精心配置线程池。可以采用动态线程池,根据任务队列长度和系统负载动态调整线程数。对于长时间运行的任务,可以将其放入独立的线程池,避免影响其他短任务,提高整体资源利用率。例如,在一个云平台中,不同类型的异步任务(如虚拟机创建、监控数据采集等)放入不同的线程池,合理分配资源。
- 当系统资源有限时,无论是
线程池配置优化
- 线程池大小
- 根据任务类型和系统资源来确定。对于CPU密集型任务,线程池大小一般设置为
CPU核心数 + 1
,如加密解密任务。对于I/O密集型任务,线程池大小可以设置为CPU核心数 * 2
甚至更大,如文件读取、网络请求任务。例如,在一个图片处理系统中,图片的压缩任务属于CPU密集型,线程池大小设置为CPU核心数 + 1
;而图片的上传下载任务属于I/O密集型,线程池大小可设置为CPU核心数 * 2
。
- 根据任务类型和系统资源来确定。对于CPU密集型任务,线程池大小一般设置为
- 队列类型
- 使用有界队列(如
ArrayBlockingQueue
),可以防止任务队列无限增长导致内存耗尽。例如在一个高并发的订单处理系统中,设置ArrayBlockingQueue
的大小为1000,当任务队列满时,可以通过拒绝策略(如CallerRunsPolicy
,让调用者线程来执行任务)来处理新任务,保证系统的稳定性。
- 使用有界队列(如
- 拒绝策略
- 除了
CallerRunsPolicy
,还可以使用AbortPolicy
(默认,直接抛出异常)、DiscardPolicy
(丢弃任务)、DiscardOldestPolicy
(丢弃队列中最老的任务)。例如在一个消息推送系统中,如果任务执行非常重要不能丢弃,可以使用CallerRunsPolicy
;如果任务不太重要且系统负载高,可以使用DiscardPolicy
。
- 除了