CodeGym /课程 /JAVA 25 SELF /异步任务:thenApply、thenAccept、thenRun

异步任务:thenApply、thenAccept、thenRun

JAVA 25 SELF
第 55 级 , 课程 1
可用

1. 启动异步任务:supplyAsync 和 runAsync

最常见的启动异步任务的方式是使用 CompletableFuture.supplyAsync。该方法接收一个返回结果的 lambda 或方法。例如,我们想要模拟从服务器加载数据:

import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 模拟耗时操作(例如,下载文件)
            sleep(1000);
            return "来自服务器的数据";
        });

        System.out.println("任务已启动!");
        // ... 在任务执行期间,这里可以先做点别的
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}

runAsync:当不需要结果时

如果你的任务不返回任何结果(例如,只是写日志、发送通知),请使用 runAsync

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    sleep(500);
    System.out.println("操作完成!");
});

runAsync 始终返回 CompletableFuture<Void>,因为它不期望有结果。

2. thenApply、thenAccept、thenRun:有什么区别?

当异步任务结束后,我们通常希望对结果做点事情。为此提供了这些“处理器”方法:

  • thenApply —— 转换结果并返回一个新的结果。
  • thenAccept —— 接收结果但不返回任何值(用于产生副作用)。
  • thenRun —— 不接收结果,也不返回任何值(仅在任务结束后执行一个动作)。

thenApply:处理并转换结果

当你需要转换前一个任务的结果时,使用 thenApply。例如:加载了一段字符串,现在想获取它的长度:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");

CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
    System.out.println("正在计算字符串长度…");
    return s.length();
});

// lengthFuture 现在包含 Integer —— 字符串 "Java" 的长度
lengthFuture.thenAccept(len -> System.out.println("长度:" + len));

发生了什么:

  • future 含有字符串 "Java"
  • thenApply 将字符串转换为它的长度(int)。
  • thenAccept 输出结果。

thenAccept:对结果执行动作(不返回值)

如果只需要对结果做点事情(比如打印到屏幕),且不需要返回值——使用 thenAccept

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "你好,世界!");

future.thenAccept(result -> {
    System.out.println("结果:" + result);
});

thenAccept 就像一个“消费者”:它消费结果并对其做一些有用的事。

thenRun:不关心结果的动作

如果你只想在任务完成后执行一个动作,但并不需要结果,请使用 thenRun

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "完成!");

future.thenRun(() -> {
    System.out.println("加载完成!");
});

请注意:在 thenRun 内无法获取前一任务的结果——它会被忽略。

3. 调用链:把任务构建成流水线

CompletableFuture 的强大之处在于可以构建计算链。每个方法(thenApplythenAcceptthenRun)都会返回一个新的 CompletableFuture,你可以继续在其上添加处理器。

示例:多阶段处理

让我们完善一下示例应用:先加载数据,再转换,之后输出结果,最后记录日志表示全部结束。

CompletableFuture.supplyAsync(() -> {
    System.out.println("步骤 1:加载数据…");
    sleep(500);
    return "Java";
})
.thenApply(data -> {
    System.out.println("步骤 2:转换数据…");
    return data.toUpperCase();
})
.thenAccept(result -> {
    System.out.println("步骤 3:输出结果:" + result);
})
.thenRun(() -> {
    System.out.println("步骤 4:全部完成!");
});

控制台输出:

步骤 1:加载数据…
步骤 2:转换数据…
步骤 3:输出结果:JAVA
步骤 4:全部完成!

请注意:
每一步都会在前一步完成之后才开始。这使我们能够构建真正的数据处理“流水线”。

4. 异步变体:thenApplyAsync、thenAcceptAsync、thenRunAsync

默认情况下,处理器(thenApplythenAcceptthenRun)会在前一个任务完成所在的同一线程里执行。有时这并不理想——如果处理较重,最好将其放到其他线程中执行。

为此提供了异步版本:

  • thenApplyAsync
  • thenAcceptAsync
  • thenRunAsync

有什么不同?

  • 无 Async: 处理器可能在与前一任务相同的线程中执行(例如,如果任务在 ForkJoinPool 中完成,处理器也可能在那里执行)。
  • 有 Async: 处理器将保证在 ForkJoinPool 的其他线程中执行(或在你的 Executor 中)。

示例:比较普通处理器与异步处理器

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("加载中... [" + Thread.currentThread().getName() + "]");
    return "Hello";
});

future.thenApply(result -> {
    System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
    return result + " World";
});

future.thenApplyAsync(result -> {
    System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
    return result + " Async World";
});

典型输出:

加载中... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]

结论:
异步处理器会在另一条线程中执行。

何时使用 Async 方法?

  • 当处理较为耗资源(例如复杂计算、网络 I/O)。
  • 当你不希望阻塞前一任务结束所在的线程。
  • 当你希望显式管理线程(例如,传入自定义的 Executor 作为第二个参数)。

5. 有用的细节

表:thenApply、thenAccept、thenRun 方法对比

方法 是否使用结果? 是否返回值? 用途
thenApply
结果转换
thenAccept
副作用(输出、日志)
thenRun
仅在任务结束后执行一个动作
thenApplyAsync
相同,但在另一条线程中
thenAcceptAsync
相同,但在另一条线程中
thenRunAsync
相同,但在另一条线程中

问:如何构建更长的链?

可以像搭 LEGO 一样一个接一个地调用方法:

CompletableFuture.supplyAsync(() -> "42")
    .thenApply(Integer::parseInt)
    .thenApply(x -> x * 2)
    .thenAccept(x -> System.out.println("结果:" + x));

输出:

结果:84

每一步都会接收上一步的结果,可以对其进行修改或仅仅使用它。

6. 使用 thenApply、thenAccept、thenRun 时的常见错误

错误 1:对返回值类型的混淆。
thenApply 必须返回一个值以便传递给链中的后续步骤。如果你误用了 thenApply 却没有返回结果,下一步可能会收到 null(或者根本无法通过编译)。对于副作用,请使用 thenAcceptthenRun

错误 2:试图在 thenRun 中使用结果。
thenRun 内无法访问前一任务的结果。如果你需要使用结果,请选择 thenApplythenAccept

错误 3:阻塞主线程。
如果你在主线程中调用 get()join(),就会失去异步的优势:线程会等待任务结束,像传统的同步代码一样。更好的做法是使用非阻塞的调用链与回调。

错误 4:未处理异常。
如果链中发生了异常,而你没有添加处理器(exceptionallyhandlewhenComplete),异常可能会“悄然消失”,任务也许以错误结束而你却看不到。请务必在调用链中处理异常。

错误 5:意外地在其他线程执行。
异步方法(如 thenApplyAsync 等)可能在另一条线程中执行。如果你访问了未做并发保护的变量,可能会发生数据竞争。

1
任务
JAVA 25 SELF, 第 55 级, 课程 1
已锁定
游戏世界的黄金储备:成就记录
游戏世界的黄金储备:成就记录
1
任务
JAVA 25 SELF, 第 55 级, 课程 1
已锁定
秘密信息解密:侦察链
秘密信息解密:侦察链
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION