同步与异步?从一个卡顿的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;}});}
}
看看这次发生了什么神奇的变化:
fetch...Async
方法不再直接返回String
,而是返回一个CompletableFuture<String>
。这就像给出去一张“提货单”,承诺未来会有一个String
结果。CompletableFuture.supplyAsync()
是关键!它会从Java的公共线程池(ForkJoinPool)里找一个空闲的线程来执行我们传进去的任务(那个lambda表达式),而原来的主线程则不会被阻塞,可以继续往下走。- 主线程发起了两个异步调用后,使用
CompletableFuture.allOf(...).join()
来等待这两个“提货单”都兑现。虽然这里join()
会阻塞主线程,但关键在于,两个耗时2秒的任务是并行执行的!所以,这里的等待时间不再是2+2=4
秒,而是max(2, 2) = 2
秒! - 执行日志会清晰地显示,查询用户和查询订单是在不同的“异步线程”里执行的,而“主线程”在把任务派发出去后,只负责最后的等待和组装。
这样一来,我这个API的性能直接提升了一倍!服务器的线程也能更快地被释放,去为其他用户服务,服务的吞吐能力大大增强。
四、我的个人感悟与总结(Java版)
这次从同步到异步的改造,让我对Java后端服务开发有了更深的理解。
- 同步:写法简单,逻辑清晰,但它是性能杀手,尤其是在处理I/O密集型(网络请求、数据库访问、文件读写)任务时。一个设计不良的同步接口,足以拖垮整个服务。
- 异步:通过将耗时任务交给其他线程处理,可以极大地释放主线程(或请求处理线程),从而提高应用的吞吐量和伸缩性。
CompletableFuture
让Java的异步代码变得优雅,告别了过去复杂的Thread
和Callback
写法。
简单来说,在Java后端的世界里,同步是“一个个来,处理完我再接待下一个”,而异步是“你们的需求我都收到了,我找了一堆帮手同时去办,谁办好了谁就给我结果”。
对于追求高性能、高并发的后端服务来说,合理地运用异步编程,已经不是一个“加分项”,而是一个“必需项”。
好了,用Java的视角重新走了一遍同步与异步的踩坑之路,是不是感觉更具体了?本质上,无论是前端还是后端,我们都是在想办法“榨干”计算机的性能,别让它在无谓的等待中浪费生命。
不知道各位Java开发的同学,在工作中还有哪些使用CompletableFuture
的心得或者踩过的坑呢?评论区见!