如果我不在中间收集,为什么 sendAsync HttpClient 会按顺序工作?

Why does sendAsync HttpClient work sequentially if I'm not do collect inbetween?

在做关于 JDK11 HttpClient 的教程时,使用 https://httpstat.us/500?sleep=1000 端点在 1 秒后返回 HTTP 500,我准备了以下代码:

HttpClient client = HttpClient.newHttpClient();

var futures = Stream.of(
        "https://httpstat.us/500?sleep=1000",
        "https://httpstat.us/500?sleep=1000",
        "https://httpstat.us/500?sleep=1000"
).map(link -> client
        .sendAsync(
                newBuilder(URI.create(link)).GET().build(),
                HttpResponse.BodyHandlers.discarding()
        ).thenApply(HttpResponse::statusCode)
).collect(Collectors.toList());

futures.stream().map(CompletableFuture::join).forEach(System.out::println);

它工作正常。程序执行大约需要 1.5 秒,所有三个调用的输出同时在终端中呈现 - 一切都很好。

但是当我将其更改为

HttpClient client = HttpClient.newHttpClient();

Stream.of(
        "https://httpstat.us/500?sleep=1000",
        "https://httpstat.us/500?sleep=1000",
        "https://httpstat.us/500?sleep=1000"
).map(link -> client
        .sendAsync(
                newBuilder(URI.create(link)).GET().build(),
                HttpResponse.BodyHandlers.discarding()
        ).thenApply(HttpResponse::statusCode)
).map(CompletableFuture::join).forEach(System.out::println);

它似乎不再异步工作 - 三个 500 正在一个接一个地显示,每个之前延迟 1 秒。

为什么?我在这里错过了什么?

这是因为 Java Stream 上的 map 方法是 “中间操作”,因此 懒惰。这意味着传递给它的 Function 不会在流的元素上调用,直到它的下游消耗元素。

这在名为 "Stream operations and pipelines" 的 Java 文档部分中进行了描述(我的评论添加在方括号中):

Intermediate operations return a new stream. They are always lazy; executing an intermediate operation such as filter() [or map()] does not actually perform any filtering [or mapping], but instead creates a new stream that, when traversed, contains the elements of the initial stream that match the given predicate [or are transformed by the given function in the case of map()]. Traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed.

在这种情况下,这意味着在使用流之前不会发出请求。

在第一个示例中,collect() 是消耗流的终端操作。结果是 CompletableFuture 个对象的列表,代表 运行 个请求。

在第二个例子中,forEach是终端操作,它一个一个地消耗流的每个元素。因为 join 操作包含在该流中,所以每个 join 在元素传递给 forEach 之前完成。后续元素按顺序使用,因此在前一个请求完成之前甚至不会发出每个请求。