如果你写过JavaScript,肯定对 async/await 的舒服劲儿印象深刻。它能让你用写同步代码的方式,去处理恶心的异步逻辑。那么问题来了,老牌大哥Java有没有类似的东西?

答案是:有,而且Java的异步发展路线和JavaScript惊人地像,最终也搞出了类似 async/await 的清爽体验。

咱们今天就把这事儿从头到尾扒一遍。

一、Java的异步进化史

Java搞异步也是一步步摸索过来的,整个过程可以分成三个阶段。

阶段一:原始时代 - ThreadFuture

最早,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对象。这个对象有三种状态:

  1. Pending (进行中):初始状态,不知道结果是啥。
  2. Fulfilled (已成功):操作成功了,对象里面包着成功的结果。
  3. 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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐