当前位置: 首页 > news >正文

同步与异步?从一个卡顿的Java服务说起

同步与异步?从一个卡顿的Java服务说起

一、故事的开始:那个让所有请求都“排队”的API接口

大家可以把场景想象成这样:我当时在开发一个后端微服务,其中有个API接口,需要调用另外两个外部服务(比如,查询用户信息和查询用户订单)来聚合数据,然后再返回给客户端。

刚开始,我也是个“直肠子”,思路很直接:先调A服务,等结果;再调B服务,等结果;最后合并,返回。

用Java代码来描述,大概就是下面这个样子(这是一个典型的Spring Boot Controller的例子):

@RestController
public class MyController {@GetMapping("/userData")public Map<String, Object> getUserData() {System.out.println("主线程:" + Thread.currentThread().getName() + " 开始处理请求...");// 1. 调用用户服务 (同步阻塞)String userInfo = fetchUserInfo(); // 假设这个方法要花2秒// 2. 调用订单服务 (同步阻塞)String orderInfo = fetchOrderInfo(); // 这个方法也要花2秒// 3. 聚合数据并返回Map<String, Object> result = new HashMap<>();result.put("user", userInfo);result.put("orders", orderInfo);System.out.println("主线程:" + Thread.currentThread().getName() + " 处理请求完毕!");return result;}// 模拟一个耗时的网络调用private String fetchUserInfo() {try {System.out.println("开始查询用户信息...");Thread.sleep(2000); // 模拟2秒的延迟System.out.println("用户信息查询完毕!");return "我是小巫";} catch (InterruptedException e) {Thread.currentThread().interrupt();return null;}}// 模拟另一个耗时的网络调用private String fetchOrderInfo() {try {System.out.println("开始查询订单信息...");Thread.sleep(2000); // 模拟2秒的延迟System.out.println("订单信息查询完毕!");return "一份咖啡订单";} catch (InterruptedException e) {Thread.currentThread().interrupt();return null;}}
}

代码写完,本地运行测试,接口能通,数据也能正确返回。但问题马上就暴露了:这个接口的总耗时是2秒 + 2秒 = 4秒

在服务器上,处理Web请求的线程(比如Tomcat的线程)是宝贵的资源。我这种写法,意味着一个请求会霸占一个线程整整4秒钟!如果并发量一上来,几十个用户同时请求,服务器的线程池很快就会被占满,新来的请求就只能排队等着,甚至直接超时失败。

这就和前端页面卡死是一个道理,只不过从“浏览器卡死”变成了“服务器线程被阻塞,吞吐量极低”。

二、煮咖啡的智慧(Java版)

咱们还是用“煮咖啡和刷牙洗脸”的例子。

  • 同步:你(主线程)先调用fetchUserInfo(),然后就站在原地等2秒,拿到结果后,再调用fetchOrderInfo(),再傻等2秒。总共花了4秒,这期间你这个线程啥也干不了。
  • 异步:你(主线程)跟A服务说“你去查用户信息,查完告诉我”,然后马上又跟B服务说“你去查订单,查完也告诉我”。你把这两个任务都“派发”出去后,你这个主线程就可以先歇着了,甚至可以去响应别的、更快的请求。等A和B都回复你了,你再把结果拼起来。理论上,总耗时只取决于最慢的那个任务,也就是2秒
三、回到代码:用CompletableFuture拯救我的服务

在现代Java中(Java 8及以后),CompletableFuture是实现异步编程的绝佳利器,它就好比JavaScript里的Promise。我们来看看如何用它来改造我的API。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class MyAsyncController {@GetMapping("/userDataAsync")public Map<String, Object> getUserDataAsync() throws ExecutionException, InterruptedException {System.out.println("主线程:" + Thread.currentThread().getName() + " 开始处理请求...");// 1. 异步调用用户服务CompletableFuture<String> userFuture = fetchUserInfoAsync();// 2. 异步调用订单服务CompletableFuture<String> orderFuture = fetchOrderInfoAsync();// 3. 等待所有异步任务完成// allOf会等userFuture和orderFuture都完成后再继续CompletableFuture.allOf(userFuture, orderFuture).join();// 4. 从Future中获取结果并聚合Map<String, Object> result = new HashMap<>();result.put("user", userFuture.get());result.put("orders", orderFuture.get());System.out.println("主线程:" + Thread.currentThread().getName() + " 处理请求完毕!");return result;}// 模拟一个异步的耗时网络调用private CompletableFuture<String> fetchUserInfoAsync() {// supplyAsync会把任务提交到ForkJoinPool线程池中执行return CompletableFuture.supplyAsync(() -> {try {System.out.println("异步线程(用户): " + Thread.currentThread().getName() + " 开始查询...");Thread.sleep(2000); // 模拟2秒的延迟return "我是小巫";} catch (InterruptedException e) {return null;}});}// 模拟另一个异步的耗时网络调用private CompletableFuture<String> fetchOrderInfoAsync() {return CompletableFuture.supplyAsync(() -> {try {System.out.println("异步线程(订单): " + Thread.currentThread().getName() + " 开始查询...");Thread.sleep(2000); // 模拟2秒的延迟return "一份咖啡订单";} catch (InterruptedException e) {return null;}});}
}

看看这次发生了什么神奇的变化:

  1. fetch...Async方法不再直接返回String,而是返回一个CompletableFuture<String>。这就像给出去一张“提货单”,承诺未来会有一个String结果。
  2. CompletableFuture.supplyAsync()是关键!它会从Java的公共线程池(ForkJoinPool)里找一个空闲的线程来执行我们传进去的任务(那个lambda表达式),而原来的主线程则不会被阻塞,可以继续往下走。
  3. 主线程发起了两个异步调用后,使用CompletableFuture.allOf(...).join()来等待这两个“提货单”都兑现。虽然这里join()会阻塞主线程,但关键在于,两个耗时2秒的任务是并行执行的!所以,这里的等待时间不再是2+2=4秒,而是max(2, 2) = 2秒!
  4. 执行日志会清晰地显示,查询用户和查询订单是在不同的“异步线程”里执行的,而“主线程”在把任务派发出去后,只负责最后的等待和组装。

这样一来,我这个API的性能直接提升了一倍!服务器的线程也能更快地被释放,去为其他用户服务,服务的吞吐能力大大增强。

四、我的个人感悟与总结(Java版)

这次从同步到异步的改造,让我对Java后端服务开发有了更深的理解。

  • 同步:写法简单,逻辑清晰,但它是性能杀手,尤其是在处理I/O密集型(网络请求、数据库访问、文件读写)任务时。一个设计不良的同步接口,足以拖垮整个服务。
  • 异步:通过将耗时任务交给其他线程处理,可以极大地释放主线程(或请求处理线程),从而提高应用的吞吐量和伸缩性CompletableFuture让Java的异步代码变得优雅,告别了过去复杂的ThreadCallback写法。

简单来说,在Java后端的世界里,同步是“一个个来,处理完我再接待下一个”,而异步是“你们的需求我都收到了,我找了一堆帮手同时去办,谁办好了谁就给我结果”

对于追求高性能、高并发的后端服务来说,合理地运用异步编程,已经不是一个“加分项”,而是一个“必需项”。


好了,用Java的视角重新走了一遍同步与异步的踩坑之路,是不是感觉更具体了?本质上,无论是前端还是后端,我们都是在想办法“榨干”计算机的性能,别让它在无谓的等待中浪费生命。

不知道各位Java开发的同学,在工作中还有哪些使用CompletableFuture的心得或者踩过的坑呢?评论区见!

http://www.dtcms.com/a/291201.html

相关文章:

  • 文字检测到文字识别
  • 如何用 Z.ai 生成PPT,一句话生成整套演示文档
  • 自反馈机制(Self-Feedback)在大模型中的原理、演进与应用
  • 【PTA数据结构 | C语言版】哥尼斯堡的“七桥问题”
  • 【ROS1】07-话题通信中使用自定义msg
  • (9)机器学习小白入门 YOLOv:YOLOv8-cls 技术解析与代码实现
  • 选择排序 冒泡排序
  • LinkedList与链表(单向)(Java实现)
  • android studio 远程库编译报错无法访问远程库如何解决
  • 算法提升之字符串回文问题-(马拉车算法)
  • Java基础教程(011):面向对象中的构造方法
  • 模拟高负载测试脚本
  • Flink框架:keyBy实现按键逻辑分区
  • 250kHz采样率下多信号参数设置
  • mysql-5.7 Linux安装教程
  • 无人机报警器技术要点与捕捉方式
  • Anaconda 路径精简后暴露 python 及工具到环境变量的配置记录 [二]
  • Linux学习之Linux系统权限
  • scratch音乐会开幕倒计时 2025年6月中国电子学会图形化编程 少儿编程 scratch编程等级考试一级真题和答案解析
  • Git核心功能简要学习
  • 知识 IP 的突围:从 “靠感觉” 到 “系统 + AI” 的变现跃迁
  • 网络编程及原理(八)网络层 IP 协议
  • 关于校准 ARM 开发板时间的步骤和常见问题:我应该是RTC电池没电了才导致我设置了重启开发板又变回去2025年的时间
  • Xilinx FPGA XCKU115‑2FLVA1517I AMD KintexUltraScale
  • 【Java EE】多线程-初阶-Thread 类及常见方法
  • Netty中CompositeByteBuf 的addComponents方法解析
  • PNP加速关断驱动电路
  • [数据结构]#4 用链表实现的栈结构
  • FastAPI 中,数据库模型(通常使用 SQLAlchemy 定义)和接口模型(使用 Pydantic 定义的 schemas)的差异
  • GraphRAG快速入门和原理理解