Loading

Future相关并发类使用

Future相关并发类使用

一、Callable&Future&FutureTask 详解

1. 基础组件对比:Runnable vs Callable

对比维度 Runnable 接口 Callable接口
返回值 无(run () 返回 void) 有(call () 返回 V 类型)
异常处理 不能抛出 checked Exception 可抛出 checked Exception(声明在 call () 方法)
函数式接口方法 void run() V call() throws Exception
配合组件 Thread(直接作为线程任务) Future/FutureTask(获结果)

2. Future 接口:任务管理工具

  • 核心 API 说明
    1. boolean cancel(boolean mayInterruptIfRunning):取消任务,参数指定是否立即中断运行中任务
    2. boolean isCancelled():判断任务是否在正常完成前被取消(是则返回 true)
    3. boolean isDone():判断任务是否完成(正常终止、异常、取消均返回 true)
    4. V get():阻塞等待任务完成,获 V 类型结果,抛 InterruptedException(线程被中断)、ExecutionException(任务执行异常)、CancellationException(任务被取消)
    5. V get(long timeout, TimeUnit unit):同上,增加超时时间,超时抛 TimeoutException
  • 本质:对 Runnable/Callable 任务的执行结果进行管理,但存在明显局限性。

3. FutureTask:Runnable 与 Future 的结合体

  • 特性:实现RunnableFuture接口(继承 Runnable 和 Future),既可以作为线程任务(Runnable)执行,也可以作为 Future 获取 Callable 的返回结果。
  • 使用流程
    1. 构建 Callable 实例;
    2. 将 Callable 实例传入 FutureTask 构造函数,创建 FutureTask 对象;
    3. 将 FutureTask 作为 Runnable 传入 Thread 或线程池执行;
    4. 通过 FutureTask 的 get () 方法获取任务结果。
  • 实战案例:促销商品信息查询
    • 同步查询问题:商品基本信息、价格、库存等 5 个接口各需 50ms,同步执行总耗时 200-300ms;
    • Future 改造方案:创建 5 个 FutureTask(分别对应 5 个查询任务),提交到线程池并行执行,总耗时约等于最长任务耗时(50ms);
    • 核心代码:通过ExecutorService提交 5 个 FutureTask,调用 get () 依次获取结果。

4. Future 的局限性

  1. 并发执行多任务时,仅能通过 get () 阻塞获取结果,无其他非阻塞等待方式;
  2. 无法对多个任务进行链式调用(如任务 A 完成后自动执行任务 B);
  3. 不能组合多个任务(如 10 个任务全部完成后执行后续操作);
  4. 无异常处理机制(接口未提供异常处理相关方法)。

二、CompletableFuture:Future 的扩展与增强

1. 核心定位

  • 实现 Future 接口,弥补 Future 的局限性,核心能力是任务编排(灵活组织任务的运行顺序、规则、方式),无需 CountDownLatch 等工具类的复杂逻辑。

2. 异步任务创建:4 个静态方法

方法签名 返回值 入参特点 线程池
runAsync(Runnable runnable) CompletableFuture 无返回值(Runnable) 默认 ForkJoinPool.commonPool ()
runAsync(Runnable runnable, Executor executor) CompletableFuture 无返回值,指定线程池 用户指定 Executor
supplyAsync(Supplier<U> supplier) CompletableFuture 有返回值(Supplier 的 get ()) 默认 ForkJoinPool.commonPool ()
supplyAsync(Supplier<U> supplier, Executor executor) CompletableFuture 有返回值,指定线程池 用户指定 Executor
  • 注意:默认线程池ForkJoinPool.commonPool()线程数默认等于 CPU 核数(可通过 JVM 参数-Djava.util.concurrent.ForkJoinPool.common.parallelism修改),建议按业务类型创建独立线程池避免线程饥饿。

3. 结果获取:join () vs get ()

方法 异常处理 使用场景
join() 抛 unchecked 异常(无需强制处理) 不想手动处理检查异常时
get() 抛 checked 异常(InterruptedException、ExecutionException,需 try-catch 或抛出) 需显式处理异常时(推荐)

4. 核心功能:任务编排与结果处理

功能分类 关键方法 作用说明 案例效果(部分)
结果处理 whenComplete() 任务完成 / 异常时执行 Action,处理结果或异常,返回原 CompletableFuture 结果 任务正常返回则打印 “执行完成”,异常则触发 exceptionally
exceptionally() 捕获任务异常,返回默认值 算术异常时返回 “异常 xxxx”
结果转换 thenApply() 接收上一任务结果,转换类型,返回同一 CompletableFuture 100→300(100*3)
thenCompose() 接收上一结果,返回新 CompletableFuture(展开内部 Future) 随机数 n→2n(新 CompletableFuture 计算)
结果消费 thenAccept() 消费单个结果,无返回值 随机数 8→打印 “40”(8*5)
thenRun() 不关心结果,任务完成后执行 Runnable 打印 “thenRun 执行”
thenAcceptBoth() 两个任务均完成,消费两者结果,无返回值 任务 1 返回 2、任务 2 返回 1→打印 “3”
结果组合 thenCombine() 合并两个任务结果,返回新结果 9+5=14(两个随机数相加)
任务交互 applyToEither() 取两个任务中先完成的结果,转换后返回 任务 1 耗时 6s、任务 2 耗时 5s→取 5 并 ×2
acceptEither 两个线程任务相比较,先获得执行结果的,就对该结果进行下一步的消费操作。
runAfterEither 两个线程任务相比较,有任何一个执行完成,就进行下一步操作,不关心运行结果
runAfterBoth() 两个任务均完成,执行 Runnable(不关心结果) 任务 1 睡 1s、任务 2 睡 2s→打印 “均完成”
allOf() 所有任务完成后返回(无返回值,需单独查每个任务结果) future1、future2 均完成→isDone () 均为 true
anyOf() 多个任务中任一完成,返回该任务结果 future1(睡 0-5s)、future2(睡 0-1s)→返回 “world”

5. 实战案例:烧水泡茶(对比 Future)

  • 需求:最优工序为 “洗水壶(1min)→烧开水(15min)” 与 “洗茶壶(1min)→洗茶杯(2min)→拿茶叶(1min)” 并行,烧开水和拿茶叶完成后泡茶。
  • Future 实现:需创建 T1Task(依赖 T2Task 的 FutureTask 获取茶叶)、T2Task,手动管理线程,代码较繁琐;
  • CompletableFuture 实现
    1. f1(runAsync):洗水壶→烧开水;
    2. f2(supplyAsync):洗茶壶→洗茶杯→拿茶叶(返回 “龙井”);
    3. f3(thenCombine):f1 和 f2 均完成后,执行泡茶逻辑,返回 “上茶:龙井”;
    4. 最终通过 f3.join () 获取结果,代码更简洁,任务依赖关系更清晰。

关键问题

问题 1:Future 相比 Runnable 有哪些核心优势?又存在哪些无法避免的局限性?

  • 答案

    优势:1.结果获取:Runnable 无返回值,Future 可通过 get () 方法获取 Callable 任务的执行结果;

    2.任务管理:Future 提供 cancel ()(取消任务)、isCancelled ()(查询是否取消)、isDone ()(查询是否完成)等 API,可实时掌握任务状态,而 Runnable 无此能力;

    3.异常间接处理:虽然 Future 无专门异常处理方法,但 get () 会抛出 ExecutionException(封装任务执行时的异常),可间接捕获任务异常,而 Runnable 的 run () 不能抛出 checked Exception。

    局限性:1.阻塞获取结果:仅能通过 get () 阻塞等待结果,无其他非阻塞方式;

    2.无链式 / 组合能力:无法实现 “任务 A 完成→自动执行任务 B” 的链式调用,也不能组合多个任务(如 10 个任务全部完成后执行后续操作);

    3.无异常处理机制:接口未提供专门的异常处理方法,需依赖 get () 的异常抛出间接处理,不够灵活。

问题 2:CompletableFuture 如何实现 “任务依赖关系” 的编排?请结合具体方法和案例说明。

  • 答案

    CompletableFuture 通过结果转换类方法实现任务依赖(前一任务结果作为后一任务入参),核心方法为thenApply()thenCompose(),具体如下:

    1. thenApply():接收上一任务的结果,通过 Function 转换类型,返回同一 CompletableFuture(适用于同步转换场景)。

      案例:异步任务 1 返回 100(supplyAsync (() -> 100)),通过 thenApply (number -> number * 3) 将结果转换为 300,最终 get () 获取 300,整个过程依赖同一 CompletableFuture。

    2. thenCompose():接收上一任务结果,返回新的 CompletableFuture(适用于异步转换场景,可展开内部 Future,避免嵌套)。

      案例:异步任务 1 生成随机数 n(supplyAsync (() -> new Random ().nextInt (30))),thenCompose (param -> CompletableFuture.supplyAsync (() -> param * 2)) 创建新 CompletableFuture 计算 2n,最终返回 2n 的结果,实现 “前一异步任务→后一异步任务” 的依赖。

问题 3:在 “促销活动商品信息查询” 场景中,同步查询存在什么问题?使用 Future 改造的核心优势是什么?具体如何实现?

  • 答案
    1. 同步查询问题:商品信息(基本信息、价格、库存、图片、销售状态)分布在不同系统,每个接口耗时约 50ms,同步查询需依次调用 5 个接口,总耗时 200-300ms,响应速度慢,影响用户体验。
    2. Future 改造的核心优势:将同步串行查询改为并行查询,总耗时约等于单个接口的最长耗时(约 50ms),大幅提升查询效率。
    3. 具体实现
      • 步骤 1:定义 5 个 Callable 任务(T1Task 查基本信息、T2Task 查价格、T3Task 查库存、T4Task 查图片、T5Task 查销售状态),每个任务 sleep 50ms 模拟接口耗时,返回查询结果;
      • 步骤 2:创建 5 个 FutureTask 对象,分别封装上述 Callable 任务;
      • 步骤 3:创建固定线程池(ExecutorService executorService = Executors.newFixedThreadPool (5)),提交 5 个 FutureTask 并行执行;
      • 步骤 4:通过 FutureTask 的 get () 方法获取每个任务的结果,最终汇总商品信息,总耗时约 50ms。
posted @ 2025-10-01 18:56  流火无心  阅读(7)  评论(0)    收藏  举报