为啥即使我不调用 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
的整个想法是它们会立即被安排启动(尽管您无法可靠地判断它们将在哪个线程中执行),并且当您到达 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
可能已经完成。 get
和 join
只是 block 直到所有阶段都完成,它不会触发计算;立即安排计算。
一个小的补充是您可以完成CompletableFuture
,而无需等待管道的执行完成:如complete
、completeExceptionally
、obtrudeValue
(即使它已经完成,也可以设置它) 、obtrudeException
或 cancel
。这是一个有趣的例子:
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(() -> try Thread.sleep(1_000L); catch (InterruptedException e) e.printStackTrace(); System.out.println("Hello"); , Executors.newSingleThreadExecutor());
看看会发生什么。
完全正确,因为在这种情况下异步执行没有特定的执行程序,所以它使用 forkJoinPool。但你就在那里,所以我会编辑那部分以避免混淆
您似乎在这里混淆了很多概念,而实际上这很简单。我建议您阅读什么是守护线程(它不是低优先级线程)。线程的优先级完全不同。以上是关于为啥即使我不调用 get() 或 join(),这个 CompletableFuture 也能工作?的主要内容,如果未能解决你的问题,请参考以下文章
为啥这个对 C# 方法的 jquery ajax GET 调用不起作用?
为啥在我们已经从内部 SELECT 中找到数据之后调用 JOIN?
为啥即使我不滚动,dispatch_semaphore_wait() 也一直返回 YES?
缓存 GET 调用的 RESTful API 结果的最佳方法
CompletableFutureCompletableFuture测试runAsync()方法调用CompletableFuture.join()/get()方法阻塞主线程