首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
当前位置:
首页>>
技术小册>>
Java并发编程实战
小册名称:Java并发编程实战
### 23 | Future:如何用多线程实现最优的“烧水泡茶”程序? 在Java并发编程的广阔领域中,`Future` 接口及其实现类是处理异步计算结果的重要工具。通过 `Future`,我们可以在不阻塞当前线程的情况下,启动一个或多个后台任务,并在未来某个时间点获取这些任务的执行结果。这一特性在处理如“烧水泡茶”这样的多任务并行问题时尤为有效,它允许我们优化时间管理,提高程序的整体效率。 #### 引言:烧水泡茶问题的背景 “烧水泡茶”是一个经典的并发编程示例,旨在说明如何通过合理安排任务的执行顺序和并行度来减少总耗时。在这个场景中,我们有以下几个步骤需要执行: 1. **烧水**:一个耗时较长的任务,假设需要5分钟。 2. **准备茶具和茶叶**:一个相对较短的任务,假设需要1分钟。 3. **泡茶**:需要水烧开后才能进行,假设泡茶本身也需要1分钟。 显然,如果顺序执行这些任务,总耗时将是7分钟(5分钟烧水 + 1分钟准备 + 1分钟泡茶)。然而,通过并发编程,我们可以让“准备茶具和茶叶”在“烧水”的同时进行,从而减少总耗时至6分钟(烧水和准备茶具茶叶同时进行,然后泡茶)。 #### 使用Future优化烧水泡茶程序 在Java中,`Future` 接口为我们提供了一种机制来异步地执行长时间运行的任务,并在将来某个时刻获取其结果。结合 `ExecutorService`,我们可以轻松地实现“烧水泡茶”程序的并发优化。 ##### 第一步:定义任务 首先,我们需要定义三个任务:烧水(`BoilWaterTask`)、准备茶具和茶叶(`PrepareTeaTask`)、泡茶(`BrewTeaTask`)。为了简化,这里假设这些任务都实现了 `Callable<Void>` 接口(虽然泡茶可能更自然地接受一个参数如热水,但为了演示 `Future` 的使用,我们暂时忽略这一点)。 ```java Callable<Void> boilWaterTask = () -> { // 模拟烧水过程 try { TimeUnit.MINUTES.sleep(5); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return null; }; Callable<Void> prepareTeaTask = () -> { // 模拟准备茶具和茶叶的过程 try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return null; }; // 注意:实际泡茶任务可能需要一个表示“水已烧开”的Future或直接传递热水作为参数 Callable<Void> brewTeaTask = () -> { // 假设水已经烧开,这里仅模拟泡茶过程 try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return null; }; ``` ##### 第二步:使用ExecutorService提交任务 接下来,我们使用 `ExecutorService` 提交这些任务,并获取它们的 `Future` 对象。由于“烧水”和“准备茶具和茶叶”可以并行执行,我们将它们同时提交。 ```java ExecutorService executor = Executors.newFixedThreadPool(2); // 创建一个固定大小的线程池 Future<Void> futureWater = executor.submit(boilWaterTask); Future<Void> futurePreparation = executor.submit(prepareTeaTask); // 等待准备茶具和茶叶完成(可选,因为烧水可能更耗时) try { futurePreparation.get(); // 如果需要确保在泡茶前准备完成 } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); // 处理异常,如取消后续任务等 } // 注意:在实际应用中,泡茶任务可能需要检查水是否烧开,这里为了演示简化处理 // 理论上,泡茶任务应该在水烧开后才开始执行 ``` ##### 第三步:泡茶并处理结果 由于“泡茶”任务依赖于“水烧开”这一条件,理论上我们应该在水烧开后才执行泡茶任务。然而,由于 `Future` 本身的机制并不直接支持依赖关系(如 CompletableFuture),我们在这里做一个简化的处理:假设我们已经知道水何时烧开(通过外部机制或简单的延时等待)。 在实际应用中,你可能会使用 `CompletableFuture` 或其他并发工具来更优雅地处理这种依赖关系。但为了保持本示例聚焦于 `Future` 的使用,我们在这里不深入展开。 ```java // 假设水已经烧开,现在泡茶 // 注意:这里实际上并没有使用Future来直接控制泡茶任务,因为它通常依赖于外部条件(水烧开) // 但在真实场景中,你可能会根据futureWater的完成情况来决定是否泡茶 // 模拟泡茶 try { // 这里只是模拟,实际中泡茶任务可能更复杂 TimeUnit.MINUTES.sleep(1); System.out.println("Tea is ready!"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 关闭ExecutorService executor.shutdown(); ``` #### 深入讨论:Future的局限性与CompletableFuture的改进 虽然 `Future` 提供了异步执行和结果查询的基本功能,但它也存在一些局限性: 1. **非阻塞获取结果但不支持取消**:`Future.get()` 会阻塞直到任务完成,且一旦调用就无法取消。 2. **不支持链式调用和组合**:`Future` 不支持像 `CompletableFuture` 那样的链式调用和组合操作,使得处理复杂的异步逻辑变得困难。 3. **缺乏异常处理能力**:`Future.get()` 在任务执行过程中抛出异常时,会重新抛出这些异常,但无法直接在任务执行过程中处理它们。 为了克服这些限制,Java 8 引入了 `CompletableFuture`,它提供了更丰富的API来支持异步编程,包括非阻塞的取消、链式调用、组合多个 `CompletableFuture` 以及更灵活的异常处理机制。 在“烧水泡茶”的例子中,如果使用 `CompletableFuture`,我们可以更优雅地处理任务间的依赖关系,如在水烧开后自动触发泡茶任务,同时保持代码的清晰和可维护性。 #### 结论 通过 `Future`,我们可以在Java中实现“烧水泡茶”程序的并发优化,减少总耗时。然而,为了更灵活地处理异步编程中的复杂场景,推荐使用 `CompletableFuture` 或其他更现代的并发工具。这些工具不仅提供了更强大的功能,还通过更简洁的API降低了编写并发程序的难度。在实际开发中,根据具体需求选择合适的并发工具是至关重要的。
上一篇:
22 | Executor与线程池:如何创建正确的线程池?
下一篇:
24 | CompletableFuture:异步编程没那么难
该分类下的相关小册推荐:
Java语言基础12-网络编程
经典设计模式Java版
Java语言基础4-数组详解
Java面试指南
Java语言基础1-基础知识
Java语言基础8-Java多线程
SpringBoot合辑-高级篇
Java性能调优实战
Java语言基础9-常用API和常见算法
Mybatis合辑5-注解、扩展、SQL构建
Java语言基础16-JDK8 新特性
SpringBoot零基础到实战