如何在Java中异步调用方法

173

我最近一直在研究Go语言中的协程(goroutines),并且认为在Java中也拥有类似功能会很不错。根据我的搜索结果,常见的实现方法是通过以下方式进行方法调用并行化:

final String x = "somethingelse";
new Thread(new Runnable() {
           public void run() {
                x.matches("something");             
    }
}).start();

这不是很优雅。 有更好的方法吗? 在项目中,我需要这样的解决方案,所以我决定围绕异步方法调用实现自己的包装类。

我在 J-Go 上发布了我的包装类。但我不知道它是否是一个好的解决方案。使用起来很简单:

SampleClass obj = ...
FutureResult<Integer> res = ...
Go go = new Go(obj);
go.callLater(res, "intReturningMethod", 10);         //10 is a Integer method parameter
//... Do something else
//...
System.out.println("Result: "+res.get());           //Blocks until intReturningMethod returns

或者更简洁:

Go.with(obj).callLater("myRandomMethod");
//... Go away
if (Go.lastResult().isReady())                //Blocks until myRandomMethod has ended
    System.out.println("Method is finished!");

我内部使用了一个实现了Runnable接口的类,并进行一些反射工作以获取正确的方法对象并调用它。

我想听听关于我的小型库和在Java中进行此类异步方法调用的意见。这样做是否安全?是否已经有更简单的方法了?


1
你能再次展示一下J-Go库的代码吗? - msangel
我正在处理一个项目,其中我逐个字符地读取流。一旦读取完整个单词,我就对该单词执行许多操作。最后,我将其放入集合中。一旦读取了来自流的所有数据,我就返回响应。我决定为每次对单词执行操作启动线程。但是它降低了整体性能。然后我知道线程本身就是昂贵的操作。我不确定启动线程以并发调用方法是否会增加任何性能,除非它执行任何重型IO操作。 - Amit Kumar Gupta
2
太好了!Java 8为此提供了CompletableFutures。其他答案可能基于旧版本的Java。 - TriCore
13个回答

197

我刚刚发现有一种更干净的方法可以做你的

new Thread(new Runnable() {
    public void run() {
        //Do whatever
    }
}).start();

(至少在Java 8中),您可以使用lambda表达式将其缩短为:

new Thread(() -> {
    //Do whatever
}).start();

就像在JS中创建一个函数一样简单!


你的回答解决了我的问题 - http://stackoverflow.com/questions/27009448/prevent-client-repeatedly-sending-requests。虽然在我的情况下有点棘手,但最终还是解决了 :) - Deckard
如何带参数调用? - yatanadam
2
@yatanadam 这可能会回答你的问题。只需将上述代码放入一个方法中,并像通常一样传递变量即可。请查看我为您制作的测试代码 - user3004449
1
@eNnillaMS 线程在运行后必须停止吗?它是自动停止的,还是由垃圾回收器停止的? - user3004449
@user3004449 线程会自动停止,但是你也可以强制停止它。 - Shawn
显示剩余3条评论

127

Java 8 引入了 CompletableFuture,它位于包 java.util.concurrent.CompletableFuture 中,可用于进行异步调用:

CompletableFuture.runAsync(() -> {
    // method call or code to be asynch.
});

另一个答案中提到了CompletableFuture,但是那个答案使用了整个supplyAsync(...)链。这是一个完美适合问题的简单包装器。 - ndm13
2
ForkJoinPool.commonPool().execute() 的开销略微较小。 - Mark Jeronimus

34

除了考虑使用第一个代码示例中提到的类,您也可以考虑使用java.util.concurrent.FutureTask类。

如果您正在使用Java 5或更高版本,则FutureTask是“可取消异步计算”的现成实现。

java.util.concurrent软件包中还提供了更丰富的异步执行调度行为(例如,ScheduledExecutorService),但FutureTask可能具备您所需的所有功能。

我甚至敢说,自从FutureTask出现以后,不建议再使用您提供的第一个代码示例。 (假设您正在使用Java 5或更高版本。)


问题在于我只想执行一个方法调用。这样我就不必更改目标类的实现。我想要的是能够直接调用,而无需担心实现Runnable或Callable。 - Felipe Hummel
我明白你的意思。Java目前还没有一流函数,所以这就是现在的技术水平。 - shadit
谢谢“future”关键字……现在我正在打开有关它们的教程……非常有用。 :D - gumuruh
5
据我所知,仅有FutureTask本身是不足以异步运行任何内容的。就像Carlos的例子一样,您仍需要创建一个线程或执行器来运行它。 - MikeFHay

25

我不喜欢使用反射来实现这个功能。
这不仅在一些重构过程中容易出错,而且可能会被SecurityManager拒绝。

FutureTask是java.util.concurrent包中的另一个好选择。
对于简单任务,这是我最喜欢的选项:

    Executors.newSingleThreadExecutor().submit(task);

使用Task相对于创建Thread(如果任务是Callable或Runnable)稍微简洁一些。


1
问题在于我只想执行一个方法调用。这样,我将不得不更改目标类的实现。我想要的是仅仅调用该方法,而不必担心实现Runnable或Callable。 - Felipe Hummel
如果只是这样,那就没什么帮助了 :( 但通常情况下,我更喜欢使用Runnable或Callable而不是Reflection。 - user85421
5
简短说明:最好跟踪 ExecutorService 实例,这样当需要时可以调用 shutdown() - Daniel Szalay
2
如果你这样做,你可能会得到未关闭的ExecutorService,导致JVM拒绝关闭。我建议编写自己的方法来获取单线程、时间限制的ExecutorService。 - djangofan
@djangofan 对的。此外,在极少数情况下,此代码可能会抛出RejectedExecutionException拒绝运行任务(JDK-8145304)。 - ZhekaKozlov

23
你可以使用Java8语法来处理CompletableFuture,这样你就可以基于调用异步函数的结果执行其他的异步计算。
例如:
 CompletableFuture.supplyAsync(this::findSomeData)
                     .thenApply(this:: intReturningMethod)
                     .thenAccept(this::notify);

更多细节可以在这篇文章中找到。


14
你可以使用jcabi-aspects和AspectJ中的@Async注释:
public class Foo {
  @Async
  public void save() {
    // to be executed in the background
  }
}

当您调用save()时,会启动一个新线程并执行其主体。您的主线程将继续执行而不等待save()的结果。

1
请注意,这种方法会使所有调用保存为异步调用,这可能是我们想要的,也可能不是。在只有某些对给定方法的调用应该是异步的情况下,可以使用本页面上建议的其他方法。 - bmorin
1
我能在Web应用程序中使用它吗(因为在那里管理线程是不推荐的)? - Naman

8
您可以使用 Future-AsyncResult 进行此操作。
@Async
public Future<Page> findPage(String page) throws InterruptedException {
    System.out.println("Looking up " + page);
    Page results = restTemplate.getForObject("http://graph.facebook.com/" + page, Page.class);
    Thread.sleep(1000L);
    return new AsyncResult<Page>(results);
}

参考文献: https://spring.io/guides/gs/async-method/

这篇文章介绍了如何在Spring中使用异步方法来提高应用程序的性能和可伸缩性。当我们需要执行长时间运行的操作时,例如调用外部API或处理大量数据时,异步方法可以让我们避免应用程序阻塞,并且可以更有效地利用计算资源。


14
如果您正在使用Spring Boot。 - Giovanni Toraldo

8

Java 还提供了一种很好的调用异步方法的方式。在 java.util.concurrent 中,我们有 ExecutorService 来帮助完成相同的任务。以下是初始化对象的示例代码 -

 private ExecutorService asyncExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

然后像这样调用函数-

asyncExecutor.execute(() -> {

                        TimeUnit.SECONDS.sleep(3L);}

1.) 当你只需要一个线程时,却分配了具有多个线程的线程池。 2.) 你没有关闭执行器。每个执行器都应该被正确地关闭,否则它的线程将会永远运行。 - ZhekaKozlov

3

您可以从Cactoos使用AsyncFunc

boolean matches = new AsyncFunc(
  x -> x.matches("something")
).apply("The text").get();

它将在后台执行,结果将作为Future在get()中可用。

2

也许这不是一个真正的解决方案,但现在 - 在Java 8中 - 您可以使用lambda表达式使此代码看起来至少更好一些。

final String x = "somethingelse";
new Thread(() -> {
        x.matches("something");             
    }
).start();

你甚至可以用一行代码来完成这个操作,而且它会很容易阅读。

new Thread(() -> x.matches("something")).start();

使用Java 8真的非常好。 - Mukti

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