[英]Thread vs Runnable vs CompletableFuture in Java multi threading
我正在尝试在我的 Spring Boot 应用程序中实现多线程。 我只是 Java 中多线程的初学者,在进行了一些搜索并阅读了各个页面上的文章之后,我需要澄清以下几点。 所以;
1.据我所知,我可以使用Thread
、 Runnable
或CompletableFuture
来在 Java 应用程序中实现多线程。 CompletableFuture
似乎是一种更新更简洁的方式,但Thread
可能具有更多优势。 那么,我应该坚持CompletableFuture
还是根据场景使用它们?
2.基本上我想使用CompletableFuture
向同一个服务方法发送 2 个并发请求:
CompletableFuture<Integer> future1 = fetchAsync(1);
CompletableFuture<Integer> future2 = fetchAsync(2);
Integer result1 = future1.get();
Integer result2 = future2.get();
如何同时发送这些请求,然后根据以下条件返回结果:
我怎样才能做到这一点? 我应该为此使用CompletableFuture.anyOf()
吗?
CompletableFuture
是一个位于Executor
/ ExecutorService
抽象之上的工具,它具有处理Runnable
和Thread
的实现。 您通常没有理由手动处理Thread
创建。 如果您发现CompletableFuture
不适合特定任务,您可以先尝试其他工具/抽象。
如果你想继续第一个(在更快的意义上)非空结果,你可以使用类似
CompletableFuture<Integer> future1 = fetchAsync(1);
CompletableFuture<Integer> future2 = fetchAsync(2);
Integer result = CompletableFuture.anyOf(future1, future2)
.thenCompose(i -> i != null?
CompletableFuture.completedFuture((Integer)i):
future1.thenCombine(future2, (a, b) -> a != null? a: b))
.join();
anyOf
允许您继续第一个结果,但不管它的实际值。 因此,要使用第一个非空结果,我们需要链接另一个操作,如果第一个结果是null
thenCombine
这只会在两个 futures 都完成时完成,但此时我们已经知道更快的结果是null
并且需要第二个。 当两个结果均为null
时,整体代码仍将产生null
。
请注意, anyOf
接受任意类型的期货并产生CompletableFuture<Object>
。 因此, i
是Object
类型,需要进行类型转换。 具有完整类型安全性的替代方案是
CompletableFuture<Integer> future1 = fetchAsync(1);
CompletableFuture<Integer> future2 = fetchAsync(2);
Integer result = future1.applyToEither(future2, Function.identity())
.thenCompose(i -> i != null?
CompletableFuture.completedFuture(i):
future1.thenCombine(future2, (a, b) -> a != null? a: b))
.join();
这要求我们指定此处不需要的 function,因此此代码Function.identity()
。 您也可以只使用i -> i
来表示身份 function; 这主要是一种风格选择。
请注意,大多数复杂性源于试图通过始终链接要在前一阶段完成时执行的相关操作来避免阻塞线程的设计。 上面的示例遵循此原则,因为最终的join()
调用仅用于演示目的; 如果调用者期望未来而不是被阻止,您可以轻松删除它并返回未来。
如果您无论如何都要执行最终的阻塞join()
,因为您立即需要结果值,您也可以使用
Integer result = future1.applyToEither(future2, Function.identity()).join();
if(result == null) {
Integer a = future1.join(), b = future2.join();
result = a != null? a: b;
}
这可能更容易阅读和调试。 这种易用性是即将推出的虚拟线程功能背后的动机。 当一个动作在虚拟线程上运行时,你不需要避免阻塞调用。 所以有了这个特性,如果你仍然需要在不阻塞你的调用线程的情况下返回一个CompletableFuture
,你可以使用
CompletableFuture<Integer> resultFuture = future1.applyToEitherAsync(future2, r-> {
if(r != null) return r;
Integer a = future1.join(), b = future2.join();
return a != null? a: b;
}, Executors.newVirtualThreadPerTaskExecutor());
通过为依赖操作请求一个虚拟线程,我们可以毫不犹豫地在 function 中使用阻塞式join()
调用,这使得代码更简单,事实上,类似于前面的非异步变体。
在所有情况下,如果代码不为空,代码将提供更快的结果,而无需等待第二个未来的完成。 但这并不能阻止对不必要的未来的评估。 CompletableFuture
根本不支持停止正在进行的评估。 您可以对其调用cancel(…)
,但这只会将未来的完成 state(结果)设置为“异常完成并出现CancellationException
”
因此,无论您是否调用cancel
,已经在进行的评估都将在后台继续进行,并且只会忽略其最终结果。
对于某些操作,这可能是可以接受的。 否则,您将不得不显着更改fetchAsync
的实现。 您可以直接使用ExecutorService
并submit
操作以获取支持取消中断的Future
。
但它也要求操作的代码对中断敏感,才能产生实际效果:
调用阻塞操作时,使用那些可能会中止并抛出InterruptedException
的方法,而不是 catch-and-continue。
在执行长时间运行的计算密集型任务时,偶尔轮询Thread.interrupted()
并在true
时退出。
那么,我应该坚持 CompletableFuture 还是根据场景使用它们?
好吧,它们都有不同的用途,您可能会直接或间接地使用它们:
Thread
表示一个线程,虽然在大多数情况下它可以被子类化,但您不应该这样做。 许多框架维护线程池,即它们启动多个线程,然后可以从任务池中获取任务。 这样做是为了减少线程创建带来的开销以及减少争用量(许多线程和很少的 cpu 内核意味着大量的上下文切换,因此您通常会尝试使用更少的线程来处理一个任务其他)。Runnable
是最早表示线程可以处理的任务的接口之一。 另一个是Callable
,它与Runnable
有两个主要区别:1)它可以返回一个值,而Runnable
有void
和 2)它可以抛出已检查的异常。 根据您的情况,您可以使用其中任何一种,但由于您想要获得结果,您更有可能使用Callable
。CompletableFuture
和Future
基本上是一种跨线程通信方式,即您可以使用它们来检查任务是否已经完成(非阻塞)或等待完成(阻塞)。所以在很多情况下是这样的:
Runnable
或Callable
Thread
池来执行你提交的任务Future
(一个实现是CompletableFuture
)让你检查任务的状态和结果,而不必自己同步。 但是,在其他情况下,您可能会直接向Thread
甚至子类Thread
提供Runnable
,但现在这些情况已经不那么常见了。
我怎样才能做到这一点? 我应该为此使用 CompletableFuture.anyOf() 吗?
CompletableFuture.anyOf()
将不起作用,因为您无法确定您传入的 2 个中的哪一个首先成功。
由于您首先对result1
感兴趣(顺便说一句,如果类型为int
,则不能为null
),因此您基本上想要执行以下操作:
Integer result1 = future1.get(); //block until result 1 is ready
if( result1 != null ) {
return result1;
} else {
return future2.get(); //result1 was null so wait for result2 and return it
}
您不想立即调用future2.get()
,因为这会阻塞,直到两者都完成,但您首先只对future1
感兴趣,因此如果产生结果,您就不必为future2
完成。
请注意,上面的代码不处理异常完成,并且可能还有一种更优雅的方式来编写您想要的期货,但我不记得它 atm(如果我记得我会添加它作为编辑)。
另一个注意事项:如果result1
不是 null,您可以调用future2.cancel()
但我建议您首先检查取消是否有效(例如,您很难真正取消 Web 服务请求)以及结果是什么中断服务将是。 如果可以让它完成并忽略结果,那可能是 go 的更简单方法。
那么,我应该坚持
CompletableFuture
还是根据场景使用它们?
使用最适合场景的那个。 显然,除非您解释场景,否则我们不能更具体。
有多种因素需要考虑。 例如:
Thread
+ Runnable
没有自然的方式来等待/返回结果。 (但实现起来并不难。)Thread
对象效率低下,因为创建线程的成本很高。 线程池更好,但您不应该自己实现线程池。ExecutorService
的解决方案负责线程池并允许您使用Callable
并返回Future
。 但是对于一次性的异步计算来说,这可能有点过头了。ComputableFuture
的解决方案允许您组合异步任务。 但是,如果您不需要这样做,那么使用ComputableFuture
可能就有些矫枉过正了。如您所见...所有情况都没有单一的正确答案。
我应该为此使用
CompletableFuture.anyOf()
吗?
不。你的例子的逻辑要求你必须有future1
的结果来确定你是否需要future2
的结果。 所以解决方案是这样的:
Integer i1 = future1.get();
if (i1 == null) {
return future2.get();
} else {
future2.cancel(true);
return i1;
}
请注意,上面的代码适用于普通Future
以及 CompletableFuture。 如果您使用CompletableFuture
是因为您认为anyOf
是解决方案,那么您不需要这样做。 调用ExecutorService.submit(Callable)
会给你一个Future
...
如果您需要处理任务和/或超时抛出的异常,将会更加复杂。 在前一种情况下,您需要捕获ExecutionException
并提取其cause
异常以获取任务抛出的异常。
还有一个警告是,第二次计算可能会忽略中断并继续进行。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.