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

Why does this CompletableFuture work even when I don't call get() or join()?

我在学习时遇到了一个问题CompletableFutureget()/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

不知道case 2的runnable block为什么能运行

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


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

Thread.sleep(100);

1。第一个将以相同的方式运行,因为 get 操作会阻塞主线程。其实对于第一种情况,你连最后的睡眠时间都不需要。

输出:Hello World!


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

输出:World!


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

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

1. runnable 调用.

2. 主线程继续其进程,printing ("World!)

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

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

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

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

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

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

World!. . (0.5)[END]

I don't know why the Runnable block of case2 is working.

没有理由不工作。

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

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

之前输出

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


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

  1. 睡眠并不能判断事件是否已经发生!
  2. 你通常不知道事件发生需要多长时间,你不知道要睡多久。
  3. 如果你睡得太久,你就会有“死时间”(见下文)。
  4. 如果你睡的时间不够长,事件可能还没有发生。所以你需要一次又一次地测试和睡觉,然后......

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

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

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


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

CompletableFuture 的整个想法是它们被立即安排启动(虽然你不能可靠地告诉它们将在哪个线程中执行),并且当你到达 get 时或 join,结果可能已经准备就绪,即: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 只是 阻塞 直到所有阶段完成,它不会触发计算;立即安排计算。


一个小的补充是您可以完成 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