为啥即使我不调用 get() 或 join(),这个 CompletableFuture 也能工作?

Posted

技术标签:

【中文标题】为啥即使我不调用 get() 或 join(),这个 CompletableFuture 也能工作?【英文标题】:Why does this CompletableFuture work even when I don't call get() or join()?为什么即使我不调用 get() 或 join(),这个 CompletableFuture 也能工作? 【发布时间】:2021-04-12 18:38:33 【问题描述】:

我在学习CompletableFuture 时遇到了一个问题。 get()/join() 方法是阻塞调用。如果我不打电话给他们怎么办?

此代码调用get():

// Case 1 - Use get()
CompletableFuture.runAsync(() -> 
    try 
        Thread.sleep(1_000L);
     catch (InterruptedException e) 
        e.printStackTrace();
    
    System.out.println("Hello");
).get();
System.out.println("World!");

Thread.sleep(5_000L); // Don't finish the main thread

输出:

Hello
World!

这段代码既不调用get()也不调用join()

// Case 2 - Don't use get()
CompletableFuture.runAsync(() -> 
    try 
        Thread.sleep(1_000L);
     catch (InterruptedException e) 
        e.printStackTrace();
    
    System.out.println("Hello");
);
System.out.println("World!");

Thread.sleep(5_000L); // For don't finish main thread

输出:

World!
Hello

我不知道为什么案例 2 的可运行块正在工作。

【问题讨论】:

为什么不希望它运行? @LouisWasserman 我了解到的材料中没有任何内容是我没有写下来的。所以我预计它不会起作用。类似于 Stream Api 的终端操作。 @LouisWasserman 像 Reactive Streams 这样的设计很常见,对于初学者来说,“推”和“拉”方法之间的区别并不总是很明显。 好问题。 “拉动”行为的其他示例:在 Python 中,生成器在执行之前实际上不会做任何事情。如果您创建了一个生成器但不运行它,则不会发生任何事情。 Rust 中的期货也是如此。它们仅在您 .await 他们时运行。 @chrylis-cautiouslyoptimistic- 没错。我不明白“拉”和“推”的方法。让我们了解一下这两个概念。谢谢。 【参考方案1】:

CompletableFuture 的整个想法是它们会立即被安排启动(尽管您无法可靠地判断它们将在哪个线程中执行),并且当您到达 getjoin 时,结果可能已经准备好,即:CompletableFuture 可能已经完成。在内部,一旦管道中的某个阶段准备就绪,该特定的CompletableFuture 将被设置为完成。例如:

String result = 
   CompletableFuture.supplyAsync(() -> "ab")
                    .thenApply(String::toUpperCase)
                    .thenApply(x -> x.substring(1))
                    .join();

与以下内容相同:

CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "ab");
CompletableFuture<String> cf2 = cf1.thenApply(String::toUpperCase);
CompletableFuture<String> cf3 = cf2.thenApply(x -> x.substring(1));
String result = cf3.join();

当您实际调用join 时,cf3 可能已经完成。 getjoin 只是 block 直到所有阶段都完成,它不会触发计算;立即安排计算。


一个小的补充是您可以完成CompletableFuture,而无需等待管道的执行完成:如completecompleteExceptionallyobtrudeValue(即使它已经完成,也可以设置它) 、obtrudeExceptioncancel。这是一个有趣的例子:

 public static void main(String[] args) 
    CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> 
        System.out.println("started work");
        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
        System.out.println("done work");
        return "a";
    );

    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
    cf.complete("b");
    System.out.println(cf.join());

这将输出:

started work
b

所以即使工作开始了,最终的值是b,而不是a

【讨论】:

更准确地说,那些工厂方法将立即安排行动或供应商,而CompletionStage 方法将在满足先决条件时立即安排。相比之下,CompletableFuture 本身的思路就不同了;顾名思义,它只是一个可以完成的未来,即当你通过默认构造函数构造一个实例时,除非有人明确地完成它,否则什么都不会发生。你的答案的第二部分不应该忘记cancel,这不过是一个例外的完成。【参考方案2】:

我不知道为什么 case2 的 Runnable 块有效。

没有理由不工作。

runAsync(...) 方法表示异步执行任务。假设应用程序不会提前结束,那么任务最终会完成,无论您是否等待它完成

CompletableFuture 提供了多种等待任务完成的方式。但是在您的示例中,您没有将其用于此目的。相反,您的 main 方法中的 Thread.sleep(...) 调用具有相同的效果;即等待任务已经(可能)完成的时间足够长。所以"Hello""World"之前输出。

重申一下,get() 调用不会导致任务发生。相反,它等待已经发生


使用sleep 等待事件(例如完成任务)发生是个坏主意:

    睡眠并不能说明事件是否发生! 您通常不知道事件发生需要多长时间,也不知道要睡多久。 如果您睡得太久,您就有了“死时间”(见下文)。 如果您睡眠时间不够长,则该事件可能尚未发生。所以你需要一次又一次地测试和睡觉,然后......

即使在这个例子中,理论上也有可能1 main 中的 sleep 在任务中的 sleep 之前完成。

基本上,CompletableFuture 的目的是提供一种有效的方式来等待任务完成并交付结果。你应该使用它...

为了说明。您的应用程序在输出 "Hello""World!" 之间等待(并浪费)约 4 秒。如果您按预期使用CompletableFuture,则不会有那 4 秒的“死区时间”。


1 - 例如,某些外部代理可能能够选择性地“暂停”正在运行任务的线程。可以通过设置断点来完成...

【讨论】:

你不需要外部代理来选择性地暂停线程,高系统负载已经有相同的效果;当没有线程取得进展时,时间将继续计时,一旦应用程序再次获得 CPU 时间,允许sleep 返回。 是的。还有其他可能发生的方式。【参考方案3】:

第二种情况是“working”,因为你让主线程休眠了足够长的时间(5 秒)。工作是在引号之间,因为它并没有真正工作,只是完成了。我在这里假设代码应该输出 Hello World! 以便被认为是“正常工作”。


在这两种情况下,在主线程结束时尝试使用此睡眠时间的相同代码:

Thread.sleep(100);

1。第一个的行为方式相同,因为 get 操作阻塞了主线程。事实上,对于第一种情况,你甚至不需要最后的睡眠时间。

输出:Hello World!


2。第二种情况不会输出Hello,因为没有人告诉主线程:“嘿,等待这个完成”。这就是get() 所做的:阻止调用者以等待任务完成。没有它,并在最后设置低睡眠时间,runnable 被调用,但在主线程停止之前无法完成其工作。

输出:World!


这也是为什么在第一种情况下写Hello World!首先是runnable的输出,然后是main的一个-意味着主线程被阻塞直到get()返回),而第二个有阅读障碍的微妙迹象:World Hello!

但这不是阅读障碍,它只是执行它被告知的内容。在第二种情况下,会发生这种情况:

1. 可运行对象被称为

2. 主线程继续其进程,打印(“世界!)”

3. Sleep 时间设置:可运行 1 秒 / 主 5 秒。 (runnable 的 sleep 也可以在第二步执行,但我把它放在这里是为了澄清行为

4. 可运行任务在 1 秒后打印 ("Hello"),CompletableFuture 完成。

5. 5 秒过去了,主线程停止。

所以你的 runnable 可以打印 Hello 因为它能够在这 5 秒超时之间执行命令。

World! . . . . . .(1)Hello. . . . . . . . . . .(5)[END]

如果您将最后 5 秒的超时时间减少到 0.5 秒,例如,您会得到

World!. . (0.5)[END]

【讨论】:

好的,谢谢。那么使用 get() 的可运行代码是否有效? @SangHoonLee 在第二个示例中,它不会工作,因为它没有足够的时间来完成。 get() 只是保证调用者线程阻塞,直到可运行对象完成。没有它,它只是一个异步进程,所以主线程不会等待它:它只会调用它并继续它的进程。 连runnable的睡眠都无法完成睡眠。这并不完全正确。只需将其更改为 CompletableFuture.runAsync(() -&gt; try Thread.sleep(1_000L); catch (InterruptedException e) e.printStackTrace(); System.out.println("Hello"); , Executors.newSingleThreadExecutor()); 看看会发生什么。 完全正确,因为在这种情况下异步执行没有特定的执行程序,所以它使用 forkJoinPool。但你就在那里,所以我会编辑那部分以避免混淆 您似乎在这里混淆了很多概念,而实际上这很简单。我建议您阅读什么是守护线程(它不是低优先级线程)。线程的优先级完全不同。

以上是关于为啥即使我不调用 get() 或 join(),这个 CompletableFuture 也能工作?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个对 C# 方法的 jquery ajax GET 调用不起作用?

为啥在字符串连接上使用 os.path.join?

为啥在我们已经从内部 SELECT 中找到数据之后调用 JOIN?

为啥即使我不滚动,dispatch_semaphore_wait() 也一直返回 YES?

缓存 GET 调用的 RESTful API 结果的最佳方法

CompletableFutureCompletableFuture测试runAsync()方法调用CompletableFuture.join()/get()方法阻塞主线程