Java有类似JavaScript的async/await吗?从Promise到虚拟线程,一次聊透
意思就是,如果数据库还没查完,代码跑到这儿就会死死地卡住,动弹不得,直到结果回来。的舒服劲儿印象深刻。它能让你用写同步代码的方式,去处理恶心的异步逻辑。本质上还是在用Promise,它就是一套“外套”,让Promise用起来更像同步代码,是目前写异步代码最推荐的方式。还会返回一个新的Promise,所以你可以像链条一样把好几个异步操作串起来,解决了早期“回调地狱”的问题。这玩意儿太底层了,跟自己组
如果你写过JavaScript,肯定对 async/await
的舒服劲儿印象深刻。它能让你用写同步代码的方式,去处理恶心的异步逻辑。那么问题来了,老牌大哥Java有没有类似的东西?
答案是:有,而且Java的异步发展路线和JavaScript惊人地像,最终也搞出了类似 async/await
的清爽体验。
咱们今天就把这事儿从头到尾扒一遍。
一、Java的异步进化史
Java搞异步也是一步步摸索过来的,整个过程可以分成三个阶段。
阶段一:原始时代 - Thread
和 Future
最早,Java搞并发和异步就是直接上手操作 Thread
,也就是线程。这玩意儿太底层了,跟自己组装电脑一样,灵活是灵活,但一不小心就搞出各种线程安全问题,心累。
后来官方给了 ExecutorService
(线程池)和 Future<T>
。这个 Future
你可以把它看成是Java最早的、最简陋的Promise。你扔给线程池一个任务,它立马给你一个 Future
,算是个“未来提货券”。
但这玩意儿有个致命的坑:
// 简化代码
Future<User> userFuture = threadPool.submit(() -> {
// 在另一个线程里查数据库
return getUserFromDb();
});
// ...干点别的...
// 现在需要用户数据了
User user = userFuture.get(); // <-- 就是这个 get() 方法,太坑了
这个 get()
方法是阻塞的!意思就是,如果数据库还没查完,代码跑到这儿就会死死地卡住,动弹不得,直到结果回来。这不又回到同步的老路上了吗?说好的异步呢?
阶段二:现代进化 - CompletableFuture
为了解决 Future
的阻塞问题,Java 8 推出了一个史诗级加强版:CompletableFuture<T>
。
这东西就是Java里正儿八经对标JavaScript Promise
的核心武器,功能非常强大,彻底改变了Java的异步写法。
你看它的用法,是不是跟 Promise.then()
一模一样:
// 使用 CompletableFuture
CompletableFuture.supplyAsync(() -> {
// 异步查用户
return getUserFromDb(123);
})
.thenApply(user -> { // 相当于 .then()
// 拿到用户user,接着去查他的订单
return getOrdersForUser(user.id);
})
.thenAccept(orders -> { // 只处理结果,不返回新东西
// 拿到订单orders,打印一下
System.out.println("找到了 " + orders.size() + " 个订单");
})
.exceptionally(error -> { // 相当于 .catch()
// 任何一步出错了,都在这处理
System.err.println("出错了: " + error.getMessage());
return null;
});
System.out.println("这行代码会立刻执行,不会等上面的结果!");
CompletableFuture
让Java实现了真正意义上的非阻塞、链式异步编程。它和Promise的功能基本可以一一对应:
JavaScript (Promise ) |
Java (CompletableFuture ) |
说明 |
---|---|---|
new Promise(...) |
supplyAsync(...) |
启动一个异步任务 |
.then(result => ...) |
.thenApply(result -> ...) |
处理结果并返回一个新结果 |
.catch(error => ...) |
.exceptionally(error -> ...) |
处理异常 |
Promise.all() |
CompletableFuture.allOf() |
等所有任务完成 |
阶段三:终极形态 - 虚拟线程 (Virtual Threads)
虽然 CompletableFuture
很牛,但你看,它还是有一串 .thenApply()
这种回调的影子,代码逻辑被拆得零零散散。
Java社区也琢磨:“咱能不能也像 async/await
那样,用写同步代码的姿势,达到异步的效果呢?”
答案就是 虚拟线程 (Virtual Threads),这玩意儿在 Java 21 已经正式发布了!
虚拟线程,就是Java给出的、在开发体验上对标 async/await
的终极答案。
它的核心思想是:你用最简单、最直观的阻塞写法来写代码,JVM底层会帮你用非阻塞的方式来高效执行。
你看下面这段用了虚拟线程的代码,它看起来完全是同步阻塞的,但实际上性能非常高:
// 需要 Java 21+
void fetchUserDataAndPosts() {
// 创建一个能开启虚拟线程的执行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<User> userFuture = executor.submit(() -> {
// 这行代码看着是阻塞的,但在虚拟线程下,它不占用系统线程
return fetchUserFromServer(123);
});
Future<List<Post>> postsFuture = executor.submit(() -> {
return fetchPostsFromServer(123);
});
// .get() 仍然会“等”,但因为它在虚拟线程上,
// 等待的时候几乎不消耗任何系统资源!
User user = userFuture.get();
List<Post> posts = postsFuture.get();
System.out.println("用户名: " + user.name());
System.out.println("文章数: " + posts.size());
}
}
虚拟线程的魔力在于:
- 写法简单:跟写普通同步代码没区别,没有回调,没有
.then()
。 - 错误处理简单:直接用你最熟悉的
try-catch
就行。 - 效果牛逼:JVM在底层调度,当你的代码在等网络IO的时候,虚拟线程会被“挂起”,完全不消耗宝贵的系统线程资源。
二、所以,JS的 async/await
到底牛在哪?
聊完了Java,我们回过头再看看JS。为啥 async/await
的出现那么重要?
Promise到底是个啥?
在 async/await
之前,我们用Promise。Promise的本质,就是一个用来包着异步操作结果的特殊对象。
一个异步操作(比如网络请求)一开始,JS会立刻给你返回一个Promise对象。这个对象有三种状态:
- Pending (进行中):初始状态,不知道结果是啥。
- Fulfilled (已成功):操作成功了,对象里面包着成功的结果。
- Rejected (已失败):操作失败了,对象里面包着失败的原因。
这个状态只能从“进行中”变成“成功”或“失败”,变了之后就再也不能改了。然后你可以用 .then()
和 .catch()
方法,来预定当状态改变后,具体要干什么事。
.then()
和 .catch()
还会返回一个新的Promise,所以你可以像链条一样把好几个异步操作串起来,解决了早期“回调地狱”的问题。
Async/Await:Promise的“语法糖”
虽然Promise的链式调用已经不错了,但还是得写一堆回调函数,看着不爽。async/await
的出现就是为了解决这个“不爽”。
我们用 async/await
把上面查用户、查文章的逻辑重写一遍:
async function fetchUserData() {
try {
// 直接“等”结果,代码从上往下读,很顺
const responseUser = await fetch('https://api.example.com/user/1');
const user = await responseUser.json();
console.log("用户名:", user.name);
const responsePosts = await fetch(`https://api.example.com/posts?userId=${user.id}`);
const posts = await responsePosts.json();
console.log("他有", posts.length, "篇文章");
} catch (error) {
// try-catch直接抓所有错误,舒服
console.error("出错了!", error);
}
}
这代码读起来简直就像同步代码,非常符合直觉。它的工作原理很简单:
async
:在一个函数前加上async
,意思就是告诉JS引擎,这个函数是个异步函数,它一定会返回一个Promise。await
:这玩意儿只能在async
函数里用。它的作用就是“暂停”当前函数的执行,去等它后面的Promise出结果。注意,它只暂停当前async
函数,并不会卡死整个网页。JS引擎会先去干别的活儿,等Promise有结果了,再回来从暂停的地方继续往下执行。如果Promise成功了,await
就直接返回成功的结果;如果失败了,await
就会抛出错误,正好可以用try-catch
来抓。
说白了,async/await
本质上还是在用Promise,它就是一套“外套”,让Promise用起来更像同步代码,是目前写异步代码最推荐的方式。
总结
现在我们可以回答最开始的问题了。当有人问Java中有没有类似JS async/await
的东西时,最准确的回答是:
有。Java的
CompletableFuture
在功能上对标JS的Promise
。而Java 21里最新的虚拟线程(Virtual Threads),则在“用同步的写法实现异步的效果”这一开发体验上,完美对标了JS的async/await
。
更多推荐
所有评论(0)