Future.get() 方法调用会阻塞,这真的是可取的吗?

89
以下是伪代码片段。 下面的代码是否不符合并行异步处理的概念?
我之所以这样问,是因为在下面的代码中,主线程会提交一个任务以在不同的线程中执行。在将任务提交到队列后,它会阻塞在Future.get()方法上等待任务返回值。我宁愿在主线程中执行任务,而不是提交到另一个线程并等待结果。通过在新线程中执行任务,我获得了什么?
我知道你可以等待有限的时间等等,但如果我真的关心结果怎么办?如果有多个任务需要执行,问题会变得更糟。在我看来,我们只是同步地完成工作。我知道Guava库提供了非阻塞的监听器接口。但我想知道Future.get() API是否正确理解。如果是正确的,为什么Future.get()被设计成阻塞,从而破坏了整个并行处理的过程?
我使用Java 6。
public static void main(String[] args){

private ExectorService executorService = ...

Future future = executorService.submit(new Callable(){
    public Object call() throws Exception {
        System.out.println("Asynchronous Callable");
        return "Callable Result";
    }
});

System.out.println("future.get() = " + future.get());
}

16
你可以提交多个任务,然后等待处理。你说得没错,如果在提交每个任务之间等待结果,那么它们会按顺序逐个处理,你也不会获得任何优势。 - Yosef Weiner
2
@SkinnyJ 如果有多个任务被提交,你如何知道返回的结果是哪个任务的?我该如何等待多个任务? - TheMonkWhoSoldHisCode
3
您将获得一个Future列表,您可以通过调用isDone()方法来检查它们是否完成,或者通过调用get()方法获取它们的结果。 - John
2
最简单的方法是使用invokeAll。否则,您可以在提交其Callable时跟踪每个future,并一个接一个地获取结果。无论哪种方式,处理都是并行进行的。 - Yosef Weiner
7
重要的是,“get”不会启动执行,“submit”会启动执行,而“get”只是“等待”结果。因此,我们可以启动多个任务(多个执行器),然后在每个任务上使用“get”。这样所有的执行器都将并行运行。如果你能够在没有结果的情况下继续执行,那么可以使用观察者模式。 - Enrique
显示剩余2条评论
5个回答

83

Future提供了方法isDone(),如果计算已经完成,则不会阻塞并返回true,否则返回false。

Future.get()用于检索计算的结果。

您有几个选择:

  • 调用isDone(),如果结果已经准备好,则通过调用get()请求它,注意没有阻塞
  • 使用get()无限期阻塞
  • 使用get(long timeout, TimeUnit unit)指定超时时间阻塞

整个Future API的目的是为了轻松地从执行并行任务的线程中获取值。如上面的项目所述,这可以同步或异步完成。

缓存示例更新

以下是来自Java Concurrency In Practice的缓存实现示例,是使用Future的一个很好的用例。

  • 如果计算已经在运行,则关心计算结果的调用者将等待计算完成
  • 如果缓存中已经有结果,则调用方将收集它
  • 如果结果尚未准备好且计算尚未启动,则调用方将启动计算并在Future中包装结果以供其他调用者使用。

所有这些都可以轻松地通过Future API实现。

package net.jcip.examples;

import java.util.concurrent.*;
/**
 * Memoizer
 * <p/>
 * Final implementation of Memoizer
 *
 * @author Brian Goetz and Tim Peierls
 */
public class Memoizer <A, V> implements Computable<A, V> {
    private final ConcurrentMap<A, Future<V>> cache
            = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;

public Memoizer(Computable<A, V> c) {
    this.c = c;
}

public V compute(final A arg) throws InterruptedException {
    while (true) {

        Future<V> f = cache.get(arg);
        // computation not started
        if (f == null) {
            Callable<V> eval = new Callable<V>() {
                public V call() throws InterruptedException {
                    return c.compute(arg);
                }
            };

            FutureTask<V> ft = new FutureTask<V>(eval);
            f = cache.putIfAbsent(arg, ft);
            // start computation if it's not started in the meantime
            if (f == null) {
                f = ft;
                ft.run();
            }
        }

        // get result if ready, otherwise block and wait
        try {
            return f.get();
        } catch (CancellationException e) {
            cache.remove(arg, f);
        } catch (ExecutionException e) {
            throw LaunderThrowable.launderThrowable(e.getCause());
        }
    }
  }
}

20
那么你仍然会在一个循环中等待检查isDone(),这不也是阻塞的吗? - TheMonkWhoSoldHisCode
2
这完全取决于你的设计。你不必这样做。例如,你可以有一个线程在系统中调度事件,并定期检查结果。在这种情况下,如果结果还没有准备好,检查结果不会阻塞,线程可以继续调度事件。有许多模式可用于避免阻塞。请查看观察者模式,它可用于通知结果,还有活动对象模式和半同步-半异步、发布-订阅等。尝试阅读一些关于这些模式的资料,以了解如何使用异步编程。 - John
7
你是正确的。但我仍然想知道这是否变得不必要复杂了。一个开箱即用的监听器解决方案将非常方便。 - TheMonkWhoSoldHisCode
3
在简单的观察者模式中,这个"Listener"最有可能是观察者(Observer)。你可能可以在20分钟内自己搞定这个问题 :) 这个链接或许会对你有所帮助:http://www.tutorialspoint.com/design_pattern/observer_pattern.htm - John
非常感谢你提供的有价值的信息。非常感激 :) - TheMonkWhoSoldHisCode
显示剩余2条评论

12
下面是伪代码片段。我的问题是-下面的代码是否不符合并行异步处理的概念?
这完全取决于您的用例:
  1. 如果您真的希望在获得结果之前阻塞,请使用阻塞式的get()

  2. 如果您可以等待特定时间以了解状态而不是无限阻塞时间,请使用带超时的get()

  3. 如果您可以在将来的某个时间检查结果而不立即分析结果,请使用CompletableFuture(java 8)

    可能会被明确完成(设置其值和状态)并且可用作CompletionStage的Future,支持依赖函数和在其完成时触发的操作。

  4. 您可以从您的Runnable/Callable实现回调机制。请参阅以下SE问题:

    Java executors: how to be notified, without blocking, when a task completes?


谢谢@ravnidra。当时我没有使用1.8版本,所以CompletableFuture不在考虑范围内。但其他输入还是很相关的。 - TheMonkWhoSoldHisCode
嗨@Ravindra,有没有一种方法可以使用get()方法继续等待,同时仍然保持UI的交互。在我的情况下,如果调用了get()方法,则我的应用程序不会在Dialog上给出onBackPressed的回调。 - Om Infowave Developers

8
我想就这个问题提供我的看法,更多从理论角度出发,因为已经有一些技术性的答案了。我想基于以下评论来回答:
“让我举个例子。我提交给服务的任务最终会发起 HTTP 请求,HTTP 请求的结果可能需要很长时间。但我确实需要每个 HTTP 请求的结果。任务是在循环中提交的。如果我等待每个任务返回(获取),那么我失去了并行处理的效率,不是吗?”
这与问题中所说的内容相符。
假设你有三个孩子,你想要做一个生日蛋糕。由于你想要制作最好的蛋糕,你需要很多不同的材料来准备它。所以你将材料分成三个不同的列表,因为你居住的地方只有3家售卖不同产品的超市,并将每个孩子分配一个单独的任务,同时进行。
现在,在你开始准备蛋糕之前(再次假设你需要事先准备所有的材料),你必须等待完成最长路线的孩子。现在,你需要等待所有材料才能开始制作蛋糕是“你”的需求,而不是任务之间的依赖关系。你的孩子们一直在尽可能同时执行任务(例如,直到第一个孩子完成任务)。因此,总之,这里有并行性。
当你只有一个孩子,并将所有三个任务分配给他/她时,就是顺序执行的例子。

3
在你提供的例子中,你可能会直接在 main() 方法中运行所有内容并继续进行。
但是让我们假设你有三个计算步骤,这些步骤目前按顺序运行。仅为了理解,假设步骤1需要 t1 秒,步骤2 需要 t2 秒,步骤3 需要 t3 秒才能完成。因此总计算时间为 t1+t2+t3。还假设 t2>t1>=t3
现在假设我们使用 Future 并行执行这些三个步骤中的任务来保存每个计算结果。您可以使用非阻塞的 isDone() 调用相应的 futures 来检查每个任务是否完成。那么会发生什么呢?理论上,您的执行速度就像 t2 完成的那样快,对吧?所以我们确实从并行性中获得了一些好处。
另外,在 Java 8 中,有 CompletableFuture 支持函数式回调。

1
让我给你举个例子。我提交给服务的任务最终会发出HTTP请求,HTTP请求的结果可能需要很长时间。但是我确实需要每个HTTP请求的结果。这些任务是在循环中提交的。如果我等待每个任务返回(获取),那么我就失去了并行性,不是吗? - TheMonkWhoSoldHisCode
不要在 isDone() 返回 true 之前调用 get。这比顺序执行所有调用更好。因此,您的执行速度将仅取决于最慢的 Web 服务(包括网络等)的性能。 - ring bearer

1
如果您不关心结果,那么请创建一个新线程,并从该线程使用ExectorService API进行任务提交。这样,您的父线程即main线程将不会以任何方式阻塞,它只会创建一个新线程,然后开始进一步执行,而新线程将提交您的任务。
要创建新线程-可以通过拥有用于异步线程创建的ThreadFactory或使用java.util.concurrent.Executor的某些实现来自行完成。
如果这是在JEE应用程序中并且您正在使用Spring框架,则可以使用@async注释轻松创建新的异步线程。
希望这可以帮助您!

@VishalP 有得到一些答案吗? - hagrawal7777

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接