资源优化策略
CPU优化:优化算法,减少不必要的计算。
内存优化:合理管理内存,避免内存泄漏。
网络优化:优化网络配置,减少网络延迟。
资源优化示例
CPU优化:使用多线程或线程池来处理并发请求。
内存优化:使用内存池技术,减少频繁的内存分配和回收。
网络优化:使用高效的网络协议(如HTTP/2、WebSocket)来减少网络延迟。
以下是针对高并发场景下CPU优化的完整技术方案,包含多线程与线程池的深度实践指南
目录
2.5 使用WebFlux而不用CompletableFuture的原因
1.2 WebFlux 和 Servlet 3.0 异步支持的适用场景
2.Servlet中的DeferredResult中为什么会有CompletableFuture
3.WebAsyncTask为什么没有用 CompletableFuture
2.7 在 Spring Boot 中,使用 Servlet 3.0 或 WebFlux
2.8 Servlet 3.0 和 WebFlux 通常不能直接一起使用
5. 线程池选了Schedulers.boundedElastic()
2.10 回调(Callback)与监听(Listener)的区别解析
1.5 在异步回调机制中,主线程和工作线程的执行关系取决于具体实现方式
1.CPU优化
1.1 线程池架构设计
1.2 核心优化策略
1. 线程池参数黄金法则
如果 I/O 阻塞时间较长,可以适当增加线程数,以提高系统的吞吐量。
// 最优线程数计算公式 (N为CPU核心数)
int optimalThreads = N * (1 + WT/ST)
// WT: 平均等待时间(如IO阻塞)
// ST: 平均计算时间
// Spring Boot默认配置调优
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
executor.setMaxPoolSize(64);
executor.setQueueCapacity(1000);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("api-worker-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
配置参数解析
setCorePoolSize:
核心线程数。这是线程池中始终保持的线程数量。
设置为 Runtime.getRuntime().availableProcessors(),即 CPU 核心数。这是基于 CPU 核心数的默认值,适用于计算密集型任务。
setMaxPoolSize:
最大线程数。这是线程池中允许的最大线程数量。
设置为 64,表示在高并发情况下,线程池最多可以扩展到 64 个线程。
setQueueCapacity:
任务队列容量。当核心线程数已满时,新任务会被放入任务队列中。
设置为 1000,表示任务队列最多可以容纳 1000 个任务。
setKeepAliveSeconds:
非核心线程的空闲存活时间。如果非核心线程在指定时间内没有任务可执行,它们将被回收。
设置为 60 秒,表示非核心线程在空闲 60 秒后会被回收。
setThreadNamePrefix:
线程名称前缀。用于标识线程池中的线程。
设置为 "api-worker-",表示线程名称将以 "api-worker-" 开头。
setRejectedExecutionHandler:
拒绝执行处理器。当任务队列已满且线程池已达到最大线程数时,新任务将被拒绝。
使用 ThreadPoolExecutor.CallerRunsPolicy(),表示如果任务被拒绝,将在调用者的线程中执行任务。
initialize:
初始化线程池。确保线程池在启动时立即初始化。
这个方法是用于配置 Spring Boot 中的线程池(
ThreadPoolTaskExecutor
),以优化多线程任务的执行。线程池是 Java 中用于管理线程的工具,可以提高系统的性能和资源利用率。通过合理配置线程池的参数,可以确保系统在高并发场景下能够高效地处理任务。
1.1 为什么这样配置?
核心线程数:
核心线程数设置为 CPU 核心数,适用于计算密集型任务。如果任务主要是 I/O 阻塞,可以根据公式适当增加线程数。
最大线程数:
最大线程数设置为 64,这是一个合理的上限,可以应对高并发场景,但不会过度消耗系统资源。
任务队列容量:
任务队列容量设置为 1000,可以缓冲一定量的任务,避免任务直接被拒绝。
非核心线程的空闲存活时间:
设置为 60 秒,可以快速回收空闲线程,节省系统资源。
拒绝执行处理器:
使用
CallerRunsPolicy
,确保任务不会被丢弃,而是由调用者线程执行,避免任务丢失。
1.2 示例:根据最优线程数公式调整配置
假设你的应用主要是 I/O 阻塞任务,平均 I/O 阻塞时间为 100 毫秒,平均计算时间为 1 毫秒。假设 CPU 核心数为 8。
根据公式: optimalThreads=8×(1+1100)=8×101=808
显然,808 个线程可能过多,可以适当调整。例如,将最大线程数设置为 128 或 256,根据实际需求调整。
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
executor.setMaxPoolSize(128); // 调整最大线程数
executor.setQueueCapacity(1000);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("api-worker-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
1.3 总结
这个方法用于配置 Spring Boot 的线程池,以优化多线程任务的执行。通过合理设置核心线程数、最大线程数、任务队列容量等参数,可以确保系统在高并发场景下能够高效地处理任务。根据任务的性质(计算密集型或 I/O 阻塞型),可以使用最优线程数公式调整线程池的配置。
2. 异步非阻塞改造
2.1 异步非阻塞改造的目的
异步非阻塞改造的主要目的是提高系统的并发处理能力和响应速度,特别是在高并发场景下。通过减少线程阻塞和资源等待时间,系统可以更高效地处理更多的请求。
2.2 WebFlux 响应式编程示例
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable String id) {
return Mono.fromCallable(() -> userService.getById(id))
.subscribeOn(Schedulers.boundedElastic()) // 阻塞操作专用线程池
.timeout(Duration.ofSeconds(3));
}
代码解析
Mono.fromCallable:
将一个阻塞操作(如 userService.getById(id))包装为一个响应式流。
fromCallable 方法允许你将同步的阻塞代码转换为响应式流。
subscribeOn(Schedulers.boundedElastic()):
指定在哪个线程池中执行阻塞操作。
boundedElastic 是一个动态调整大小的线程池,适用于处理阻塞任务。
这样可以避免阻塞操作阻塞主线程,从而提高系统的响应速度。
timeout(Duration.ofSeconds(3)):
设置超时时间,如果操作在 3 秒内没有完成,将自动取消。
2.3 与 CPU 优化的关系
虽然这些代码主要关注的是异步处理和非阻塞 I/O,但它们也与 CPU 优化有间接关系:
减少线程阻塞:
通过将阻塞操作移到后台线程池,主线程可以继续处理其他请求,从而提高 CPU 的利用率。
减少线程阻塞可以避免 CPU 空闲等待,提高系统的整体性能。
动态调整线程池大小:
使用如
boundedElastic
这样的线程池,可以根据实际负载动态调整线程数量,避免过多线程消耗过多资源。
1.什么是IO
IO(Input/Output)指的是计算机与外部环境之间的数据交换过程,包括输入和输出。
包括:
1.1 网络通信
如果服务是通过网络进行通信的,例如使用HTTP、WebSocket等协议与远程服务器进行数据交换,这属于IO操作。因为这些协议涉及到数据的输入和输出,包括从网络读取数据和向网络发送数据。
1.2 本地文件操作
如果服务是通过本地文件系统进行操作的,例如读写文件,这也属于IO操作。因为文件系统是外部存储设备的一部分,涉及到数据的输入和输出。
1.3 数据库操作
如果服务是通过数据库进行数据存储和检索的,这也属于IO操作。因为数据库操作通常涉及到数据的读写操作。
1.4 其他外部设备
如果服务是通过其他外部设备(如打印机、扫描仪等)进行操作的,这也属于IO操作。因为这些设备需要进行数据的输入和输出。
2.IO操作的类型和特点
2.1 同步IO
程序在发起IO请求后会等待操作完成,期间不能执行其他任务。这种模型在传统的网络编程中常见,例如使用Socket进行通信
2.2 异步IO
程序在发起IO请求后可以继续执行其他任务,不需要等待操作完成。这种模型可以提高程序的并发性能
2.3 阻塞和非阻塞
阻塞是指程序在等待IO操作完成时不能执行其他任务;非阻塞是指程序在等待IO操作时可以继续执行其他任务
2.4 异步处理框架
WebFlux:(WebFlux 适用于响应式编程)
WebFlux 是 Spring 5 引入的响应式编程框架,专门用于构建非阻塞的 Web 应用。
它基于 Reactor 库,提供了完整的响应式编程支持。
Servlet 3.0 异步支持:(Servlet 3.0 异步支持适用于传统应用的异步改造)
Servlet 3.0 引入了异步处理支持,允许在后台线程中处理请求,而不会阻塞主线程。
这是一种标准的 Java EE 异步处理机制。
2.5 使用WebFlux而不用CompletableFuture的原因
1.WebFlux 的优势
1.1 完整的响应式编程支持
WebFlux 是 Spring 5 引入的响应式编程框架,基于 Reactor 库,提供了完整的响应式编程支持。它支持非阻塞 I/O 和响应式流(Reactive Streams),能够处理复杂的异步数据流。
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userService.getUserById(id)
.timeout(Duration.ofSeconds(3))
.switchIfEmpty(Mono.error(new UserNotFoundException("User not found")));
}
Mono 和 Flux:WebFlux 提供了 Mono 和 Flux 两种响应式类型,分别用于单个值和多个值的异步处理。
timeout:可以设置超时时间,自动取消长时间运行的操作。
switchIfEmpty:可以处理空值的情况,提供默认值或抛出异常。
1.2 集成 Spring 生态系统
WebFlux 与 Spring 生态系统深度集成,包括 Spring Data、Spring Security、Spring Web 等。这意味着你可以无缝地使用 Spring 的各种功能,而不需要额外的适配。
@Service
public class UserService {
private final ReactiveMongoTemplate mongoTemplate;
public UserService(ReactiveMongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
public Mono<User> getUserById(String id) {
return mongoTemplate.findById(id, User.class);
}
}
ReactiveMongoTemplate:WebFlux 与 Spring Data MongoDB 集成,支持响应式操作。
2). 什么是Spring Data
Spring Data 是 Spring 生态系统中一个强大的子项目,它的核心目标是:简化 Java 应用程序与各种数据存储技术(数据库、NoSQL、云存储等)交互的过程。 它通过提供一套统一的、基于 Repository 抽象概念的编程模型来实现这一目标。
以下是 Spring Data 的关键特性和解释:
2.1 统一的 Repository 抽象
- Spring Data 的核心是
Repository
接口(通常使用其子接口如CrudRepository
、PagingAndSortingRepository
、JpaRepository
)。 - 开发人员只需定义一个接口,继承自这些 Spring Data 提供的 Repository 接口,并声明所需的数据操作方法(包括查询方法签名)。
- Spring Data 会在运行时自动为你生成该接口的具体实现类(所谓的“代理对象”)。 开发者无需编写繁琐的 DAO 实现代码。
2.2 自动化 CRUD 操作:
- 基础的 CRUD(创建、读取、更新、删除)操作被内置在
CrudRepository
等接口中(如save()
,findById()
,findAll()
,deleteById()
,count()
等)。你只需要继承这些接口,这些方法就立即可用,无需任何实现代码。
2.3 声明式查询方法:
- 最具特色的功能之一。 你可以在 Repository 接口中定义方法,仅通过遵循特定的命名约定来声明查询。
- 例如:定义一个方法
List<User> findByLastName(String lastName);
。Spring Data 解析方法名(findBy
+ 属性名LastName
+ 查询条件),并将其转换为底层的查询语言(如 JPQL/SQL)。 - 这极大地减少了编写查询代码(JPQL, SQL)或 Criteria API 代码的工作量。
2.4 基于注解的查询:
- 对于更复杂的查询,或者命名约定无法满足需求时,可以使用
@Query
注解直接在 Repository 方法上手动定义查询(支持 JPQL 或原生 SQL)。 - 例如:
@Query("SELECT u FROM User u WHERE u.email = ?1") User findByEmailAddress(String emailAddress);
2.5 分页和排序支持:
PagingAndSortingRepository
提供了开箱即用的分页(Pageable
参数)和排序(Sort
参数)功能。- 例如:
Page<User> findAll(Pageable pageable);
- 开发者可以轻松实现数据的分页加载和排序
2.6 对多种数据存储技术的支持:
- Spring Data 提供了一系列模块,针对不同的数据存储技术提供了统一的编程模型:
- 关系型数据库: Spring Data JPA (基于 JPA/Hibernate), Spring Data JDBC
- NoSQL 数据库:
- 文档型:Spring Data MongoDB
- 键值型:Spring Data Redis
- 列族型:Spring Data Cassandra
- 图数据库:Spring Data Neo4j
- 搜索引擎: Spring Data Elasticsearch
- 其它: Spring Data REST (将 Repository 暴露为 RESTful API), Spring Data Gemfire 等。
- 核心价值: 无论底层使用哪种数据库,你操作数据的编程模型(Repository 接口、方法命名查询、
@Query
注解、分页参数等)都是高度一致的。
2.7 减少样板代码:
- 这是 Spring Data 最显著的优势之一。它自动处理了资源管理(连接)、事务边界管理、异常转换等繁琐的基础设施代码,让开发者专注于领域逻辑和查询声明。
Spring Data 的核心价值总结:
- 简化: 显著减少访问各种数据存储所需编写的模板代码。
- 抽象: 提供一致的数据访问编程模型,屏蔽不同数据存储底层实现的差异(在 API 层面)。
- 生产力: 通过自动化 Repository 实现和声明式查询,大大提高开发效率。
- 可扩展性: 模块化设计,支持广泛的流行数据存储技术。
2.8 典型应用场景:
假设你有一个 User
实体类(带有 @Entity
注解或映射元数据),你需要对它进行数据库操作。
1.创建一个 Repository 接口:
public interface UserRepository extends JpaRepository<User, Long> { // Long 是主键类型
// 自动继承了大量 CRUD 方法 (save, findById, findAll, deleteById...)
// 声明式查询方法:根据 lastName 查找
List<User> findByLastName(String lastName);
// 复杂查询使用 @Query
@Query("SELECT u FROM User u WHERE u.email LIKE %?1%")
List<User> findByEmailContains(String emailFragment);
// 分页查询
Page<User> findByFirstName(String firstName, Pageable pageable);
}
2.在 Service 或 Controller 中注入并使用
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null); // 使用自动生成的 findById
}
public List<User> findUsersByLastName(String lastName) {
return userRepository.findByLastName(lastName); // 使用声明式方法
}
// ... 使用其他方法
}
总之,Spring Data 是构建现代 Java 数据访问层的事实标准工具包。它以 Spring 的编程模型为基础,通过强大的 Repository 抽象和自动化实现机制,极大地简化了与各种数据存储交互的复杂性,让开发者能够专注于业务逻辑本身。
3).为什么用MyBatis而不用Spring Data
在Java持久层框架选择上,MyBatis的流行度高于Spring Data(尤其是Spring Data JPA),主要源于以下核心差异及适用场景:
3.1 核心差异对比
3.2 选择MyBatis的核心原因
1.灵活控制SQL性能
- MyBatis支持手写复杂SQL、批量操作、存储过程调用,可直接针对数据库特性优化(如索引提示、分页语法)。
- Spring Data JPA自动生成的SQL可能低效,复杂查询需额外学习HQL或Native SQL,灵活性受限
2.适应快速迭代需求
- 互联网项目常需调整SQL逻辑(如字段增减、查询条件变更),MyBatis的XML/注解配置修改直观,无需重构接口方法。
- Spring Data JPA的接口方法名约定在复杂查询时难以满足需求,且方法名过长影响可读性
3.降低团队协作成本
- SQL显式声明在Mapper文件中,便于DBA审核、性能监控和问题定位。
- Spring Data JPA的自动化机制导致SQL隐藏在框架内部,排查慢查询或逻辑错误较困难
4.无缝整合遗留系统
- 已有复杂SQL或存储过程可快速迁移到MyBatis,避免重写。
- Spring Data JPA需将SQL转换为JPQL或Entity操作,重构成本高
5.生态工具增强
- MyBatis Plus等插件提供代码生成、分页优化等功能,进一步简化开发。
- Spring Data JPA的扩展能力依赖于Hibernate特性,定制化难度较大
3.3 Spring Data JPA的适用场景
- 简单CRUD应用:方法命名约定和自动查询大幅提升基础操作效率。
- 快速原型验证:无需关注SQL细节,加速产品迭代初期开发。
- 多数据库兼容项目:抽象层支持轻松切换数据源(如MySQL→PostgreSQL
MyBatis在需要深度SQL优化、高频迭代的互联网项目中更受欢迎;而Spring Data JPA更适合数据库操作标准化、追求开发速度的传统应用。框架选择本质是控制力 vs 开发效率的权衡,
MyBatis的灵活性更契合当前高性能、可维护性优先的技术趋势。
4.非阻塞 I/O
WebFlux 基于 Netty,支持非阻塞 I/O,能够高效地处理高并发请求。这使得 WebFlux 在处理大量并发连接时比传统的阻塞 I/O 模型(如 Servlet)更具优势。
@GetMapping("/users")
public Flux<User> getAllUsers() {
return userService.getAllUsers();
}
Flux:用于处理多个值的响应式流,支持非阻塞 I/O。
5).Netty、Tomcat与Servlet
5.1 核心概念与定位
5.2 Netty 与 Tomcat 核心区别
- Tomcat 是 Servlet 容器,本质是基于 HTTP 的 Web 服务器;
- Netty 是网络框架,可构建任意协议的通信层(包括 HTTP 服务器)
5.3三者技术联系
Netty 与 Tomcat
- 功能重叠:Netty 可替代 Tomcat 的 HTTP 服务能力(如通过
HttpServerCodec
处理 HTTP 协议)。 - 本质差异:
- Tomcat 是完整的 Web 应用服务器(含会话管理、安全等);
- Netty 是底层通信框架,需额外开发才能实现类似功能
- 功能重叠:Netty 可替代 Tomcat 的 HTTP 服务能力(如通过
Netty 与 Servlet
- Netty 不直接支持 Servlet 规范,但可通过扩展(如
netty-servlet
桥接)模拟 Servlet 容器。 - 若需在 Netty 中运行 Servlet,需自行实现请求解析、线程映射等逻辑,成本较高。
- Netty 不直接支持 Servlet 规范,但可通过扩展(如
Servlet 与 Tomcat
- Servlet 是 Java 定义的接口规范(如
HttpServlet
),定义了处理 HTTP 请求的标准方式。 - Tomcat 实现了 Servlet 规范,提供运行时环境(容器),管理 Servlet 生命周期、线程池等 。
- 示例:Spring MVC 的
DispatcherServlet
最终由 Tomcat 调用执行。
- Servlet 是 Java 定义的接口规范(如
- Tomcat 依赖 Servlet 规范实现 Web 功能;
- Netty 可选择性支持 HTTP/Servlet,但非核心能力
5.4 如何选择
1.选 Tomcat
当:需快速部署标准 Web 应用(如 Spring Boot)、依赖 Servlet/JSP 生态、无需深度定制协议
2.选 Netty
当:开发高性能中间件(MQ/RPC)、需自定义二进制协议(如游戏)、追求极致吞吐与低延迟
注意:Tomcat 从 8.5+ 已全面转向 NIO 模型,性能差距缩小,但 Netty 在协议定制化上仍有压倒性优势
6. 错误处理和回压支持
WebFlux 提供了强大的错误处理机制和回压支持,能够更好地处理异步数据流中的异常情况。
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userService.getUserById(id)
.onErrorResume(UserNotFoundException.class, ex -> Mono.empty())
.switchIfEmpty(Mono.error(new UserNotFoundException("User not found")));
}
onErrorResume:捕获特定类型的异常并提供替代值。
switchIfEmpty:处理空值的情况,提供默认值或抛出异常。
7.CompletableFuture 的局限性
虽然 CompletableFuture 是 Java 8 引入的异步编程模型,但它有一些局限性,特别是在处理复杂的异步数据流时:
1. 缺少响应式流支持
CompletableFuture 本身不支持响应式流(Reactive Streams),这使得它在处理复杂的异步数据流时不够灵活。
public CompletableFuture<User> getUser(String id) {
return CompletableFuture.supplyAsync(() -> userService.getById(id));
}
CompletableFuture:适用于简单的异步任务,但不支持复杂的流处理。
2.缺少非阻塞 I/O 支持
CompletableFuture 本身不支持非阻塞 I/O,需要结合其他框架(如 Netty)来实现非阻塞 I/O。
public CompletableFuture<User> getUser(String id) {
return CompletableFuture.supplyAsync(() -> userService.getById(id), threadPool);
}
threadPool:需要手动管理线程池,以实现非阻塞 I/O。
3.错误处理不够灵活
CompletableFuture 的错误处理机制相对简单,不如 WebFlux 提供的响应式错误处理机制灵活。
public CompletableFuture<User> getUser(String id) {
return CompletableFuture.supplyAsync(() -> userService.getById(id))
.exceptionally(ex -> {
if (ex instanceof UserNotFoundException) {
return null;
}
throw new RuntimeException(ex);
});
}
exceptionally:捕获异常并提供替代值,但不如 WebFlux 的 onErrorResume 灵活。
8.选择 CompletableFuture 的场景
尽管 WebFlux 有诸多优势,但在某些场景下,CompletableFuture 仍然是一个合适的选择:
1. 简单的异步任务
如果任务相对简单,不需要复杂的响应式流处理,CompletableFuture 是一个轻量级的选择。
public CompletableFuture<User> getUser(String id) {
return CompletableFuture.supplyAsync(() -> userService.getById(id));
}
2.与现有代码集成
如果现有代码已经大量使用了 CompletableFuture,继续使用 CompletableFuture 可以减少重构成本。
public CompletableFuture<User> getUser(String id) {
return CompletableFuture.supplyAsync(() -> userService.getById(id))
.exceptionally(ex -> {
if (ex instanceof UserNotFoundException) {
return null;
}
throw new RuntimeException(ex);
});
}
9.总结
WebFlux:
适用于高并发、复杂的响应式编程场景。
提供完整的响应式编程支持、非阻塞 I/O、错误处理和回压支持。
与 Spring 生态系统深度集成。
CompletableFuture:
适用于简单的异步任务。
轻量级,易于与现有代码集成。
在实际应用中,你可以根据具体需求选择合适的工具。如果需要处理高并发和复杂的异步数据流,WebFlux 是更好的选择;如果任务简单且不需要复杂的响应式流处理,CompletableFuture 也是一个不错的选择。
2.6 消息队列与这种异步处理的区别
消息队列主要用于 异步消息传递 和 系统间解耦,而 WebFlux 和 Servlet 3.0 异步支持主要用于 异步处理 和 非阻塞 I/O。它们可以同时存在于一个系统中,用于解决不同的问题。
1.为什么在某些场景下不使用消息队列?
1.1 消息队列主要用于以下场景:
系统间解耦:允许不同的系统组件之间通过消息进行通信,而不需要直接调用。
异步任务处理:将任务放入队列,由消费者异步处理,提高系统的响应速度。
缓冲和负载均衡:通过队列缓冲消息,平滑处理突发流量。
1.2 WebFlux 和 Servlet 3.0 异步支持的适用场景
WebFlux 和 Servlet 3.0 异步支持主要用于以下场景:
高并发处理:通过非阻塞 I/O 和异步处理,提高系统的并发处理能力。
响应式编程:支持复杂的异步数据流处理,适合处理复杂的业务逻辑。
快速响应:减少线程阻塞,提高系统的响应速度。
1.3 为什么在某些场景下不使用消息队列
1. 性能考虑
消息队列引入额外的延迟:消息队列需要将消息从生产者发送到队列,再从队列发送到消费者,这会引入额外的网络延迟。
直接异步处理更快:如果任务可以直接在内存中异步处理,使用 WebFlux 或 Servlet 3.0 异步支持可以更快地响应请求,而不需要通过消息队列进行中转。
2. 系统复杂性
引入消息队列增加复杂性:消息队列需要额外的配置和维护,增加了系统的复杂性。
轻量级异步处理:如果任务相对简单,使用 WebFlux 或 Servlet 3.0 异步支持可以更简单地实现异步处理,而不需要引入消息队列。
3. 适用场景不同
消息队列适用于异步任务和解耦:如果需要将任务放入队列,由消费者异步处理,或者需要解耦系统组件,消息队列是合适的选择。
WebFlux 和 Servlet 3.0 异步支持适用于高并发和快速响应:如果需要处理高并发请求,减少线程阻塞,提高系统的响应速度,WebFlux 和 Servlet 3.0 异步支持是更好的选择。
1.4 总结
消息队列:
适用于异步任务处理和系统间解耦。
引入额外的延迟和复杂性,但提供了高可靠性和解耦能力。
WebFlux 和 Servlet 3.0 异步支持:
适用于高并发处理和快速响应。
提供非阻塞 I/O 和异步处理能力,适合处理复杂的业务逻辑。
在实际应用中,你可以根据具体需求选择合适的工具。如果需要处理复杂的异步任务和解耦系统组件,消息队列是合适的选择;如果需要处理高并发请求和快速响应,WebFlux 和 Servlet 3.0 异步支持是更好的选择。在某些情况下,也可以将它们结合起来使用,以充分发挥各自的优势。
2.Servlet中的DeferredResult中为什么会有CompletableFuture
DeferredResult
是一个低级别的工具,用于支持异步处理。它允许你在后台线程中完成异步操作,并在操作完成后设置结果。DeferredResult
本身并不管理线程池,因此你需要自己管理线程池来执行后台任务。
@RestController
public class AsyncController {
@GetMapping("/testDeferredResult")
public DeferredResult<String> testDeferredResult() {
DeferredResult<String> deferredResult = new DeferredResult<>();
// 使用 CompletableFuture 在后台线程中执行异步任务
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(5000); // 模拟耗时操作
deferredResult.setResult("Async operation completed");
} catch (Exception e) {
deferredResult.setErrorResult(e);
}
});
return deferredResult;
}
}
CompletableFuture.runAsync:
CompletableFuture.runAsync 是 Java 8 引入的异步编程模型,用于在后台线程中执行异步任务。
它可以将任务提交到默认的线程池(ForkJoinPool.commonPool())中执行。
在任务完成后,通过 deferredResult.setResult 设置异步操作的结果。
CompletableFuture
:负责在后台线程中执行具体的异步任务。
DeferredResult
:负责管理异步响应,确保在后台线程完成任务后能够正确地返回结果。
3.WebAsyncTask
为什么没有用 CompletableFuture
WebAsyncTask
是一个高级别的工具,用于支持异步处理。它封装了线程池和任务执行逻辑,因此你不需要自己管理线程池。WebAsyncTask
内部使用了 TaskExecutor
来执行任务,这使得它比 DeferredResult
更简单易用。
WebAsyncTask
内部已经封装了线程池和任务执行逻辑:WebAsyncTask
内部使用了TaskExecutor
来执行任务,你不需要手动管理线程池。WebAsyncTask
提供了超时处理机制,任务在指定时间内没有完成会自动取消。
简化代码:
使用
WebAsyncTask
可以直接实现异步任务,而不需要额外引入CompletableFuture
。这使得代码更加简洁,减少了复杂性。
3.1 总结
在实际应用中,你可以根据具体需求选择合适的工具。如果任务相对复杂,需要更灵活的线程池管理,可以使用
DeferredResult
结合CompletableFuture
。如果任务相对简单,可以直接使用WebAsyncTask
。
2.7 在 Spring Boot 中,使用 Servlet 3.0 或 WebFlux
1.使用 Servlet 3.0
当你使用 Servlet 3.0 处理异步请求时,Spring Boot 默认使用 Tomcat 作为嵌入式服务器。Tomcat 是一个传统的 Servlet 容器,支持 Servlet 3.0 的异步特性。
启动类配置
@SpringBootApplication
public class Servlet3Application {
public static void main(String[] args) {
SpringApplication.run(Servlet3Application.class, args);
}
}
Servlet 配置
@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(30000);
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(5000);
resp.getWriter().write("Async operation completed");
} catch (Exception e) {
e.printStackTrace();
} finally {
asyncContext.complete();
}
});
}
}
2.使用 WebFlux
当你使用 WebFlux 处理异步请求时,Spring Boot 默认使用 Netty 作为嵌入式服务器。Netty 是一个高性能的非阻塞 I/O 框架,非常适合处理高并发的异步请求。
启动类配置
@SpringBootApplication
public class WebFluxApplication {
public static void main(String[] args) {
SpringApplication.run(WebFluxApplication.class, args);
}
}
WebFlux 控制器
@RestController
public class WebFluxController {
@GetMapping("/users/{id}")
public Mono<String> getUser(@PathVariable String id) {
return Mono.fromCallable(() -> {
Thread.sleep(5000); // 模拟耗时操作
return "User " + id;
}).subscribeOn(Schedulers.boundedElastic());
}
}
3.如何选择
使用 Servlet 3.0:
适用于传统的 Web 应用,需要支持 Servlet 3.0 的异步特性。
使用
spring-boot-starter-web
,默认运行在 Tomcat 上。
使用 WebFlux:
适用于响应式 Web 应用,需要支持非阻塞 I/O 和高并发的异步请求。
使用
spring-boot-starter-webflux
,默认运行在 Netty 上。
4.总结
Servlet 3.0:
使用 Tomcat 作为嵌入式服务器。
适用于传统的 Web 应用,支持 Servlet 3.0 的异步特性。
WebFlux:
使用 Netty 作为嵌入式服务器。
适用于响应式 Web 应用,支持非阻塞 I/O 和高并发的异步请求
在实际应用中,你可以根据具体需求选择合适的框架和服务器。如果需要处理高并发的异步请求,建议使用 WebFlux 和 Netty;如果需要支持传统的 Servlet 3.0 特性,可以使用 Spring MVC 和 Tomcat。
2.8 Servlet 3.0 和 WebFlux 通常不能直接一起使用
1.为什么不能一起使用?
Servlet 3.0:
基于传统的 Servlet API,运行在 Servlet 容器(如 Tomcat)中。
使用同步阻塞 I/O 模型。
WebFlux:
基于响应式编程模型,运行在非阻塞 I/O 容器(如 Netty)中。
使用非阻塞 I/O 和事件驱动模型。
当同时引入 spring-boot-starter-web
和 spring-boot-starter-webflux
时,Spring Boot 的自动配置机制会尝试同时启用两种不同的 Web 框架,导致以下问题:
Bean 定义冲突:例如
requestMappingHandlerMapping
的 Bean 重复定义。配置冲突:Spring MVC 和 Spring WebFlux 的配置相互干扰,导致请求路由混乱
2.如何解决冲突?
2.1 方案一:明确指定应用类型
在 application.properties
或 application.yml
文件中,明确指定应用的 Web 类型
spring.main.web-application-type=reactive # 使用 WebFlux
# 或
spring.main.web-application-type=servlet # 使用 Spring MVC
2.2 方案二:分离配置类
为 Spring MVC 和 WebFlux 分别创建独立的配置类,避免 Bean 定义冲突:
// MVC 配置类
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
// MVC 相关配置
}
// WebFlux 配置类
@Configuration
@EnableWebFlux
public class WebFluxConfig implements WebFluxConfigurer {
// WebFlux 相关配置
}
2.3 方案三:适配器模式
通过定义一个通用的控制器接口,实现适配器模式,将 WebFlux 和 Spring MVC 的请求处理统一到一个通用接口上:
public interface CommonController {
Mono<String> handleRequest(ServerRequest request);
}
public class WebFluxControllerAdapter implements CommonController {
private final WebFluxController webFluxController;
public WebFluxControllerAdapter(WebFluxController webFluxController) {
this.webFluxController = webFluxController;
}
@Override
public Mono<String> handleRequest(ServerRequest request) {
return webFluxController.handle(request);
}
}
public class WebMvcControllerAdapter implements CommonController {
private final WebMvcController webMvcController;
public WebMvcControllerAdapter(WebMvcController webMvcController) {
this.webMvcController = webMvcController;
}
@Override
public Mono<String> handleRequest(ServerRequest request) {
return Mono.fromCallable(() -> webMvcController.handle(request.toHttpServletRequest()));
}
}
2.4 方案四:维护兼容性中间件
在 WebFlux 和 Spring MVC 之间引入一个中间层,用于处理两者之间的兼容性问题
@RestController
public class UnifiedController {
private final WebFluxController webFluxController;
private final WebMvcController webMvcController;
public UnifiedController(WebFluxController webFluxController, WebMvcController webMvcController) {
this.webFluxController = webFluxController;
this.webMvcController = webMvcController;
}
@RequestMapping("/unified")
public Mono<String> handleRequest(ServerRequest request) {
if (isWebFluxRequest(request)) {
return webFluxController.handle(request);
} else {
return Mono.fromCallable(() -> webMvcController.handle(request.toHttpServletRequest()));
}
}
private boolean isWebFluxRequest(ServerRequest request) {
// 判断请求是否为 WebFlux 请求
return request.attribute("org.springframework.web.reactive.HandlerMapping.bestMatchingHandler").isPresent();
}
}
3.总结
虽然 Spring MVC 和 WebFlux 不能直接一起使用,但可以通过以下方式解决冲突:
明确指定应用的 Web 类型。
分离配置类,避免 Bean 定义冲突。
使用适配器模式,统一请求处理接口。
引入兼容性中间件,处理两者之间的兼容性问题。
在实际应用中,建议根据具体需求选择合适的解决方案。如果项目主要是基于 Spring MVC 开发的,建议使用 Spring MVC;如果需要处理高并发的异步请求,建议使用 WebFlux。
2.9 响应式编程与传统编程的对比
1.区别
2.响应式编程的典型应用场景
- 高并发系统:如电商秒杀、消息推送系统
- 微服务通信:服务间的异步调用
- 实时数据处理:如股票行情、物联网数据
- 资源受限环境:在有限资源下处理大量请求
3.实际应用注意事项
响应式生态系统:
- 需要使用响应式数据源(如 R2DBC、WebClient)
- 避免在响应式链中使用阻塞操作
学习曲线:
- 响应式编程模型与传统编程差异较大
- 需要理解数据流、操作符和线程模型
性能调优:
- 合理选择线程池
- 监控背压和资源使用情况
调试挑战:
- 异步执行栈追踪复杂
- 需要专用的调试工具和技巧
4. 理解数据流、操作符和线程模型
4.1 数据流(Reactive Streams)
数据流是响应式编程的基础,它代表一系列按时间顺序产生的事件或值。在 Spring WebFlux 中,主要有两种数据流类型:
Mono:表示 0-1 个元素的异步序列
- 常用于返回单个结果的操作(如查询单个用户)
- 支持异步操作完成 / 错误 / 取消的通知
Flux:表示 0-N 个元素的异步序列
- 常用于返回多个结果的操作(如查询用户列表)
- 支持背压机制(消费者可以控制生产者的速度)
数据流的特点:
- 异步处理:数据在产生时被处理,无需等待整个数据集
- 非阻塞:不会阻塞调用线程
- 可组合:可以通过操作符进行链式处理
数据在产生时被处理,无需等待整个数据集?
异步处理与数据流的实时性解析:
1.异步处理的核心本质
异步处理的关键在于数据处理与数据产生的解耦,
即:数据无需等待全部生成后再统一处理,而是随生随处理。
这就像流水线作业 —— 原材料(数据)每到达一个工位(处理环节)就立即被加工,而不是等所有原材料堆积完成后再启动生产线。
2.Flux 与 Mono 的异步处理差异
Flux 的 “流式处理” 特性
Flux 代表 0-N 个元素的序列,数据会按时间顺序逐个产生。例如:
- 从数据库查询 100 条用户记录时,Flux 会在获取到第 1 条数据后立即开始处理(如转换格式、过滤条件),同时继续获取第 2 条、第 3 条……
- 处理逻辑(如
map
、filter
操作符)会随数据产生而实时执行,无需等待所有 100 条数据全部返回。Mono 的 “单值处理” 特性
Mono 代表 0-1 个元素,数据要么不存在,要么是一个完整的单值。例如:
- 查询单个用户时,Mono 会在获取到完整用户对象后才触发处理逻辑(如校验权限、序列化响应)。
- 由于数据是 “一次性” 交付的,不存在 “分批次产生” 的过程,因此无需体现 “随生随处理” 的特性,但依然属于异步处理(获取数据的过程是非阻塞的)。
3.异步处理的核心优势
资源利用率更高:
无需为等待全部数据而占用线程资源(如传统同步编程中List<T>
需等所有数据加载完才处理),线程可在数据产生间隙处理其他任务。响应更及时:
前端用户能更快看到部分数据结果(如分页加载时先显示前 10 条,同时加载后 10 条),而非白屏等待整个数据集返回。支持背压(Backpressure):
当数据产生速度超过处理速度时,消费者可通知生产者减缓发送,避免内存溢出(如 Flux 处理大文件流时)。通过这种设计,响应式编程既能处理 “海量数据流” 的实时响应,也能兼顾 “单值操作” 的异步优化,最终实现系统的高吞吐量和低延迟。
4.2 操作符(Operators)
操作符是处理数据流的工具,它们允许你转换、过滤、组合和处理数据流中的元素。常见的操作符包括:
转换操作:
map
:将每个元素转换为另一个值flatMap
:将每个元素转换为一个新的数据流,并合并结果transform
:应用一个自定义的转换函数
过滤操作:
filter
:根据条件过滤元素take
:只取前 N 个元素skip
:跳过前 N 个元素
组合操作:
concat
:连接多个数据流merge
:合并多个并行的数据流zip
:将多个数据流的元素配对
错误处理:
onErrorReturn
:发生错误时返回默认值onErrorResume
:发生错误时提供备用数据流retry
:发生错误时重试操作
副作用操作:
doOnNext
:对每个元素执行副作用操作doOnComplete
:在完成时执行副作用操作doOnError
:在发生错误时执行副作用操作
4.3 线程模型(Schedulers)
线程模型决定了响应式操作在哪个线程或线程池中执行。在 Spring WebFlux 中,主要有以下几种调度器:
Schedulers.immediate():
- 在当前线程中立即执行
- 不切换线程上下文
Schedulers.single():
- 使用单个线程执行所有任务
- 适合需要顺序执行的操作
Schedulers.parallel():
- 使用固定大小的线程池(默认 CPU 核心数)
- 适合计算密集型操作
Schedulers.boundedElastic():
- 使用可伸缩的线程池(最大线程数为 10000)
- 适合可能阻塞的 IO 操作
- 线程会在空闲 60 秒后回收
Schedulers.fromExecutorService():
- 使用自定义的 ExecutorService
- 适合需要精细控制线程池参数的场景
5. 线程池选了Schedulers.boundedElastic()
避免在响应式链中使用阻塞操作为啥选了Schedulers.boundedElastic():适用于可能阻塞的 IO 操作这不是矛盾吗?
5.1 关于线程池选择的 "矛盾" 解释
return Mono.fromCallable(() -> userService.getById(id))
.subscribeOn(Schedulers.boundedElastic()) // 阻塞操作专用线程池
.timeout(Duration.ofSeconds(3));
这里的设计实际上是一种 "策略性阻塞",原因如下:
业务逻辑的现实:
- 在实际项目中,我们经常需要调用一些不可避免的阻塞 API(如传统的 JDBC、第三方同步 API 等)
- 完全避免阻塞在某些场景下是不现实的
隔离策略:
boundedElastic()
线程池的设计初衷就是为了隔离阻塞操作- 它不会影响主事件循环(如 Netty 的 EventLoop 线程)
- 确保阻塞操作不会导致整个系统失去响应
资源控制:
boundedElastic()
线程池有最大线程数限制(默认 10000)- 当达到最大线程数时,新任务会进入队列等待
- 防止无限制创建线程导致系统崩溃
过渡策略:
- 这是从传统同步代码迁移到响应式编程的一种过渡策略
- 随着项目演进,可以逐步替换为真正的响应式实现
5.2 最佳实践建议
优先使用响应式 API:
- 尽可能使用响应式数据源(如 R2DBC、WebClient)
- 避免在响应式链中使用阻塞 API
隔离阻塞操作:
- 如果必须使用阻塞 API,使用
boundedElastic()
隔离 - 不要在主事件循环中执行阻塞操作
- 如果必须使用阻塞 API,使用
设置超时和背压:
- 为阻塞操作设置合理的超时时间
- 对可能产生大量数据的操作实现背压机制
监控和调优:
- 监控线程池使用情况
- 根据实际负载调整线程池参数
逐步迁移:
- 对于大型项目,采用渐进式迁移策略
- 先隔离阻塞操作,再逐步替换为响应式实现
2.10 回调(Callback)与监听(Listener)的区别解析
1.回调(Callback)
1.1 定义:
回调是一种函数式编程模式,允许一个对象(调用者)将函数(回调函数)传递给另一个对象(被调用者),当特定事件发生时,被调用者执行该回调函数。
1.2 特点:
- 同步 / 异步均可:回调可以是同步的(立即执行)或异步的(稍后执行)
- 主动调用:被调用者主动调用回调函数
- 一对一关系:通常一个操作对应一个回调
- 代码嵌入:回调逻辑直接嵌入调用者代码中
1.3 同步回调
// 回调接口
interface CalculatorCallback {
void onResult(int result);
}
// 被调用者
class Calculator {
public void calculate(int a, int b, CalculatorCallback callback) {
int result = a + b;
callback.onResult(result); // 主动调用回调
}
}
// 调用者
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
// 定义并传递回调
calculator.calculate(5, 3, result -> {
System.out.println("计算结果: " + result);
});
}
}
Java中回调机制与Lambda表达式的结合使用
(1)回调接口定义
interface CalculatorCallback {
void onResult(int result); // 抽象方法:用于接收计算结果
}
作用:定义回调规范,约定结果处理方式(类似JavaScript的回调函数)。
关键点:单方法接口(SAM),可被Lambda简化。
(2)被调用者(Calculator)
class Calculator {
public void calculate(int a, int b, CalculatorCallback callback) {
int result = a + b;
callback.onResult(result); // 触发回调
}
}
(3)调用者(Main)
calculator.calculate(5, 3, result -> {
System.out.println("计算结果: " + result);
});
传统写法(匿名内部类):
calculator.calculate(5, 3, new CalculatorCallback() {
@Override
public void onResult(int result) {
System.out.println("计算结果: " + result);
}
});
Lambda简化:直接以(参数) -> {逻辑}替代匿名类,代码更简洁。
Java编译器自动推断result为int类型(因CalculatorCallback.onResult(int)已定义参数类型)
设计思想与优势
1.解耦计算与处理
Calculator只负责计算,不关心结果如何被使用(如打印、存储、网络发送等)。
2.灵活扩展
调用者可通过不同Lambda实现不同处理逻辑(如替换为result -> saveToDatabase(result))。
3.函数式编程特性
将代码作为参数传递,符合"行为参数化"思想。
注意事项
接口限制:Lambda仅适用于单方法接口(如Runnable、Comparator等)。
变量捕获:Lambda可访问外部final或等效final的局部变量(Java 8+隐式final)。确保线程安全性和变量值的一致性(Lambda可能在不同线程中执行)
实例变量不受限:Lambda可直接访问和修改类的成员变量(非局部变量)。
并发风险:若Lambda捕获可变变量,多线程下可能引发数据竞争
值捕获:Lambda捕获的是变量的值副本,而非变量本身,因此需保证值不变性。
线程安全:强制不可变性避免多线程环境下的竞态条件
调试复杂性:Lambda的堆栈跟踪可能比匿名类更难阅读。
5. 扩展应用场景
异步编程:结合CompletableFuture实现非阻塞回调。
事件监听:GUI开发中(如Android点击事件)。
集合操作:Java Stream API的forEach()、map()等方法均依赖Lambda。
1.4 异步回调
class AsyncService {
public void processAsync(AsyncCallback callback) {
// 模拟异步操作
new Thread(() -> {
try {
Thread.sleep(1000);
callback.onSuccess("操作完成");
} catch (Exception e) {
callback.onError(e);
}
}).start();
}
}
interface AsyncCallback {
void onSuccess(String result);
void onError(Exception e);
}
在这段代码中,调用方(Caller)是指使用AsyncService并实现AsyncCallback接口的代码。
被调用方 AsyncService 类 提供异步执行能力,触发回调
调用方 实现AsyncCallback接口并调用processAsync()的类 定义业务逻辑,处理成功/失败结果
调用方示例代码
public class Main {
public static void main(String[] args) {
AsyncService service = new AsyncService();
// 调用方实现回调接口
service.processAsync(new AsyncCallback() {
@Override
public void onSuccess(String result) {
System.out.println("成功: " + result); // 调用方处理成功逻辑
}
@Override
public void onError(Exception e) {
System.err.println("失败: " + e.getMessage()); // 调用方处理错误逻辑
}
});
System.out.println("异步操作已启动,主线程继续执行...");
}
}
Lambda简化:调用方可用Lambda替代匿名类(需Java 8+)
service.processAsync(result -> System.out.println("成功: " + result),
error -> System.err.println("失败: " + error));
设计思想
控制反转(IoC):调用方通过回调接口将处理逻辑注入被调用方。
异步解耦:调用方无需阻塞等待结果,适合IO密集型操作。
1.5 在异步回调机制中,主线程和工作线程的执行关系取决于具体实现方式
1. 线程执行模型
2.基于回调的异步调用
在这种方式中,调用方需要提供一个回调接口的实现,以便在异步操作完成时被调用
public interface Callback {
void onComplete(String result);
}
public class AsyncService {
public void doAsyncWork(Callback callback) {
new Thread(() -> {
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 操作完成,调用回调
callback.onComplete("操作完成");
}).start();
}
}
public class Main {
public static void main(String[] args) {
AsyncService service = new AsyncService();
// 调用方实现回调接口
service.doAsyncWork(new Callback() {
@Override
public void onComplete(String result) {
System.out.println("回调结果: " + result);
}
});
System.out.println("主线程继续执行...");
}
}
在这个例子中,调用方必须实现Callback
接口,以便接收异步操作的结果。
3.基于Future的异步调用
在这种方式中,调用方不需要实现任何接口,而是通过Future
对象来获取异步操作的结果。
import java.util.concurrent.*;
public class AsyncService {
public Future<String> doAsyncWork() {
return Executors.newSingleThreadExecutor().submit(() -> {
// 模拟耗时操作
Thread.sleep(2000);
return "操作完成";
});
}
}
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
AsyncService service = new AsyncService();
// 调用异步方法,获取Future对象
Future<String> future = service.doAsyncWork();
// 主线程继续执行其他逻辑
System.out.println("主线程继续执行...");
// 等待异步操作完成并获取结果
String result = future.get();
System.out.println("异步操作结果: " + result);
}
}
在这个例子中,调用方不需要实现任何接口,而是通过Future
对象来获取异步操作的结果。
4.基于CompletableFuture的异步调用
CompletableFuture
是Java 8引入的一种更强大的异步编程工具,支持链式调用和组合操作
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class AsyncService {
public CompletableFuture<String> doAsyncWork() {
return CompletableFuture.supplyAsync(() -> {
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "操作完成";
});
}
}
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
AsyncService service = new AsyncService();
// 调用异步方法,获取CompletableFuture对象
CompletableFuture<String> future = service.doAsyncWork();
// 主线程继续执行其他逻辑
System.out.println("主线程继续执行...");
// 等待异步操作完成并获取结果
String result = future.get();
System.out.println("异步操作结果: " + result);
}
}
在这个例子中,调用方同样不需要实现任何接口,而是通过CompletableFuture对象来处理异步操作的结果。
5.基于事件监听的异步调用
在这种方式中,调用方需要注册一个监听器来接收事件通知,但监听器的实现可以是独立的。
public interface EventListener {
void onEvent(String event);
}
public class EventSource {
private List<EventListener> listeners = new ArrayList<>();
public void registerListener(EventListener listener) {
listeners.add(listener);
}
public void triggerEvent(String event) {
for (EventListener listener : listeners) {
listener.onEvent(event);
}
}
}
public class Main {
public static void main(String[] args) {
EventSource eventSource = new EventSource();
// 调用方实现监听器接口
EventListener listener = new EventListener() {
@Override
public void onEvent(String event) {
System.out.println("收到事件: " + event);
}
};
// 注册监听器
eventSource.registerListener(listener);
// 触发事件
eventSource.triggerEvent("事件发生");
}
}
在这个例子中,调用方需要实现EventListener接口,但这个实现可以是独立的,不一定需要在调用方法时直接提供。
6.总结
基于回调的异步调用:调用方必须实现回调接口。
基于Future或CompletableFuture的异步调用:调用方不需要实现任何接口,而是通过Future或CompletableFuture对象来处理异步结果。
基于事件监听的异步调用:调用方需要实现监听器接口,但实现可以是独立的。
因此,是否需要实现调用接口取决于具体的异步调用机制和设计模式。
2.监听
2.1 定义
监听是观察者模式的实现,通过注册监听器到事件源,当事件发生时,事件源通知所有已注册的监听器。
2.2 特点
- 纯异步:基于事件驱动,事件发生时才触发
- 被动通知:监听器被动接收事件,不关心事件何时发生
- 一对多关系:一个事件源可以有多个监听器
- 松耦合:通过接口或抽象类解耦
2.3 示例
// 事件类
class Event {
private String message;
public Event(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
// 事件监听器接口
interface EventListener {
void onEvent(Event event);
}
// 事件源
class EventSource {
private List<EventListener> listeners = new ArrayList<>();
// 注册监听器
public void addListener(EventListener listener) {
listeners.add(listener);
}
// 移除监听器
public void removeListener(EventListener listener) {
listeners.remove(listener);
}
// 触发事件
public void fireEvent(Event event) {
for (EventListener listener : listeners) {
listener.onEvent(event); // 通知所有监听器
}
}
}
// 使用监听
public class Main {
public static void main(String[] args) {
EventSource source = new EventSource();
// 注册多个监听器
source.addListener(event -> System.out.println("监听器1: " + event.getMessage()));
source.addListener(event -> System.out.println("监听器2: " + event.getMessage()));
// 触发事件
source.fireEvent(new Event("事件发生"));
}
}
2.4 Java 中两者的核心区别
2.5 Java 中的典型应用场景
回调的应用:
- Java 8 的函数式接口(如
Runnable
、Callable
) CompletableFuture
的回调方法(thenApply
、whenComplete
)- Spring 的
JdbcTemplate
回调(RowMapper
) - 单元测试中的
Mockito
回调
- Java 8 的函数式接口(如
监听的应用:
- Java Swing/AWT 的事件监听器(如
ActionListener
) - Spring 的事件机制(
ApplicationListener
) - Java NIO 的选择器(
SelectionKey.OP_READ
) - 消息队列的消费者(如 RabbitMQ 的
Channel.basicConsume
)
- Java Swing/AWT 的事件监听器(如
2.6 高级对比:Lambda 表达式与函数式接口
Java 8 引入 Lambda 表达式后,回调和监听的语法变得更加相似,但本质区别仍然存在:
// 回调示例(使用函数式接口)
Consumer<String> callback = result -> System.out.println("结果: " + result);
processDataAsync(callback);
// 监听示例(使用事件接口)
button.addActionListener(event -> {
System.out.println("按钮被点击");
});
2.7 总结
回调适合:
- 简单的异步操作结果处理
- 需要返回值或状态的场景
- 函数式编程风格的 API 设计
监听适合:
- 复杂的事件驱动系统
- 多组件间的松散耦合通信
- 需要支持事件过滤和广播的场景
在实际开发中,两者常结合使用。例如,一个监听系统可能使用回调来处理特定事件的响应逻辑。理解它们的区别有助于选择更合适的设计模式。
3. 监听系统
监听系统中,通常希望主线程能够继续执行其他任务,而不是被阻塞。因此,使用非阻塞回调(如 thenAccept
或 thenApply
)是更合适的选择。这样,主线程可以在异步任务完成之前继续处理其他事件或任务。
3.1 监听系统中的线程管理
在监听系统中,通常会有一个主线程(如事件循环线程)负责监听事件并分发任务。为了不阻塞主线程,可以将异步任务提交到后台线程池中执行,而主线程继续处理其他事件。
代码示例
以下是一个完整的示例,展示如何在监听系统中使用非阻塞回调,让主线程继续执行。
3.2.异步服务
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class AsyncService {
public CompletableFuture<String> handleEvent(String event) {
return CompletableFuture.supplyAsync(() -> {
// 模拟耗时操作
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "Event " + event + " handled successfully";
});
}
}
3.3 监听系统
public class EventListener {
private final AsyncService asyncService;
public EventListener(AsyncService asyncService) {
this.asyncService = asyncService;
}
public void onEvent(String event) {
// 启动异步任务
CompletableFuture<String> future = asyncService.handleEvent(event);
// 使用回调函数处理异步任务的结果
future.thenAccept(result -> {
System.out.println("异步任务结果: " + result);
// 执行后续操作
performPostProcessing();
});
// 主线程继续执行
System.out.println("主线程继续执行...");
}
private void performPostProcessing() {
System.out.println("执行后续操作...");
}
}
3.4 主线程(事件循环线程)
public class Main {
public static void main(String[] args) {
AsyncService asyncService = new AsyncService();
EventListener eventListener = new EventListener(asyncService);
// 模拟事件循环
while (true) {
// 模拟接收到事件
String event = "event1";
System.out.println("接收到事件: " + event);
// 处理事件
eventListener.onEvent(event);
// 主线程继续处理其他事件
try {
Thread.sleep(1000); // 模拟主线程继续处理其他任务
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.5 输出示例
假设主线程的名称是 main
,后台线程的名称是 ForkJoinPool.commonPool-worker-1
,运行上述代码可能会输出:
接收到事件: event1
主线程继续执行...
异步任务结果: Event event1 handled successfully
执行后续操作...
接收到事件: event1
主线程继续执行...
...
3.6 理解监听系统中的线程管理
主线程(事件循环线程):
主线程负责监听事件并分发任务。
主线程在调用
onEvent
方法后,不会等待异步任务完成,而是立即继续处理其他事件。
异步任务:
异步任务在后台线程中执行,不会阻塞主线程。
异步任务完成后,回调函数会在任务完成的线程中执行。
回调函数:
回调函数在异步任务完成的线程中执行,而不是主线程。
回调函数可以执行后续操作,如日志记录、通知等。
3.7 总结
在监听系统中,使用非阻塞回调(如 thenAccept
或 thenApply
)是更合适的选择,因为这样可以避免阻塞主线程,让主线程继续处理其他事件。通过合理使用回调函数,可以实现高效的异步编程,提高系统的并发处理能力和响应速度。