Java执行者:任务完成时如何在不阻塞的情况下得到通知?


154

假设我有一个队列,里面满是需要提交给执行者服务的任务。我希望他们一次处理一个。我能想到的最简单的方法是:

  1. 从队列中接任务
  2. 提交给执行者
  3. 在返回的Future上调用.get并阻塞,直到获得结果为止
  4. 从队列中执行另一个任务...

但是,我试图避免完全阻止。如果我有10,000个这样的队列,需要一次处理一个任务,那么我的堆栈空间将用完,因为它们中的大多数将保留阻塞的线程。

我想要提交一个任务并提供一个回叫,当任务完成时会调用该回叫。我将使用该回调通知作为发送下一个任务的标志。(functionaljava和jetlang显然使用了这种非阻塞算法,但我无法理解它们的代码)

如果不编写自己的执行程序服务,如何使用JDK的java.util.concurrent做到这一点?

(向我提供这些任务的队列本身可能会阻塞,但这是一个稍后要解决的问题)

Answers:


146

定义一个回调接口以接收要在完成通知中传递的任何参数。然后在任务结束时调用它。

您甚至可以为Runnable任务编写通用包装,并将其提交给ExecutorService。或者,请参见下面的Java 8内置机制。

class CallbackTask implements Runnable {

  private final Runnable task;

  private final Callback callback;

  CallbackTask(Runnable task, Callback callback) {
    this.task = task;
    this.callback = callback;
  }

  public void run() {
    task.run();
    callback.complete();
  }

}

使用CompletableFutureJava 8,Java 8包含了一种更精细的方法来组成管道,在该管道中可以异步和有条件地完成进程。这是一个人为但完整的通知示例。

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

public class GetTaskNotificationWithoutBlocking {

  public static void main(String... argv) throws Exception {
    ExampleService svc = new ExampleService();
    GetTaskNotificationWithoutBlocking listener = new GetTaskNotificationWithoutBlocking();
    CompletableFuture<String> f = CompletableFuture.supplyAsync(svc::work);
    f.thenAccept(listener::notify);
    System.out.println("Exiting main()");
  }

  void notify(String msg) {
    System.out.println("Received message: " + msg);
  }

}

class ExampleService {

  String work() {
    sleep(7000, TimeUnit.MILLISECONDS); /* Pretend to be busy... */
    char[] str = new char[5];
    ThreadLocalRandom current = ThreadLocalRandom.current();
    for (int idx = 0; idx < str.length; ++idx)
      str[idx] = (char) ('A' + current.nextInt(26));
    String msg = new String(str);
    System.out.println("Generated message: " + msg);
    return msg;
  }

  public static void sleep(long average, TimeUnit unit) {
    String name = Thread.currentThread().getName();
    long timeout = Math.min(exponential(average), Math.multiplyExact(10, average));
    System.out.printf("%s sleeping %d %s...%n", name, timeout, unit);
    try {
      unit.sleep(timeout);
      System.out.println(name + " awoke.");
    } catch (InterruptedException abort) {
      Thread.currentThread().interrupt();
      System.out.println(name + " interrupted.");
    }
  }

  public static long exponential(long avg) {
    return (long) (avg * -Math.log(1 - ThreadLocalRandom.current().nextDouble()));
  }

}

1
一眨眼三个答案!我喜欢CallbackTask,这种简单直接的解决方案。回想起来很明显。谢谢。关于其他有关SingleThreadedExecutor的评论:我可能有成千上万个队列,其中可能有成千上万的任务。他们每个人需要一次处理一个任务,但是不同的队列可以并行运行。这就是为什么我使用单个全局线程池。我是执行人的新手,所以请告诉我是否有误。
沙巴兹

5
好的模式,但是我会使用Guava的可监听的将来API,它提供了很好的实现。
Pierre-Henri

这不超过使用Future的目的吗?
takecare

2
@Zelphir这是Callback您声明的接口;不是来自图书馆。如今,根据我需要从任务传回侦听器的原因Runnable,我可能只使用,ConsumerBiConsumer
埃里克森

1
@Bhargav这是回调的典型代表-外部实体“回叫”控制实体。您是否要阻塞创建任务的线程,直到任务完成?那么在第二个线程上运行任务有什么目的呢?如果允许线程继续,它将需要重复检查某些共享状态(可能在循环中,但取决于您的程序),直到发现由true进行的更新(布尔标志,队列中的新项等)。如此答案中所述的回调。然后,它可以执行一些其他工作。
erickson

52

在Java 8中,可以使用CompletableFuture。这是我在代码中使用的示例,用于使用它从用户服务中获取用户,将其映射到视图对象,然后更新视图或显示错误对话框(这是GUI应用程序):

    CompletableFuture.supplyAsync(
            userService::listUsers
    ).thenApply(
            this::mapUsersToUserViews
    ).thenAccept(
            this::updateView
    ).exceptionally(
            throwable -> { showErrorDialogFor(throwable); return null; }
    );

它异步执行。我正在使用两种私有方法:mapUsersToUserViewsupdateView


如何将CompletableFuture与执行程序一起使用?(以限制并发/并行实例的数量)这是一个提示:cf:向执行者提交未来任务为什么要工作
user1767316

47

使用番石榴的未来可监听API并添加回调。cf. 从网站:

ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
ListenableFuture<Explosion> explosion = service.submit(new Callable<Explosion>() {
  public Explosion call() {
    return pushBigRedButton();
  }
});
Futures.addCallback(explosion, new FutureCallback<Explosion>() {
  // we want this handler to run immediately after we push the big red button!
  public void onSuccess(Explosion explosion) {
    walkAwayFrom(explosion);
  }
  public void onFailure(Throwable thrown) {
    battleArchNemesis(); // escaped the explosion!
  }
});

您好,但是如果我想在onSuccess之后停止,该线程该怎么办?
DevOps85

24

您可以扩展FutureTask类,并覆盖done()方法,然后将FutureTask对象添加到中ExecutorService,以便done()FutureTask完成后立即调用该方法。


then add the FutureTask object to the ExecutorService,您能告诉我该怎么做吗?
Gary Gauh

@GaryGauh可以查看更多信息,您可以扩展FutureTask,我们可以将其称为MyFutureTask。然后使用ExcutorService提交MyFutureTask,然后将运行MyFutureTask的run方法,当MyFutureTask完成后,将调用您的done方法。这里有些令人困惑的是两个FutureTask,实际上MyFutureTask是正常的Runnable。
赵超中

15

ThreadPoolExecutor还具有您可以覆盖和使用的beforeExecuteafterExecute钩子方法。这里是从描述ThreadPoolExecutorJavadoc中

挂钩方法

这个类提供保护的重写beforeExecute(java.lang.Thread, java.lang.Runnable)afterExecute(java.lang.Runnable, java.lang.Throwable)之前和每个任务的执行之后被调用的方法。这些可以用来操纵执行环境。例如,重新初始化ThreadLocals,收集统计信息或添加日志条目。此外,方法terminated()可以被重写以执行一旦Executor完全终止就需要执行的任何特殊处理。如果钩子或回调方法引发异常,内部工作线程可能进而失败并突然终止。


6

使用CountDownLatch

它来自java.util.concurrent,这正是等待多个线程完成执行然后继续执行的方式。

为了实现您需要的回调效果,这确实需要一些额外的额外工作。也就是说,在一个单独的线程中自己处理此问题,该线程使用CountDownLatch和并等待它,然后继续通知您需要通知的内容。没有对回调的本地支持,或类似的效果。


编辑:现在,我进一步了解了您的问题,我认为您不必要地伸手可及。如果您选择了常规SingleThreadExecutor,则将所有任务交给它,它将以本机方式进行排队。


使用SingleThreadExecutor知道所有线程已完成的最佳方法是什么?我看到了一个使用一阵子!executor.isTerminated的示例,但这似乎不是很优雅。我为每个工人实现了回调功能,并增加了有效的计数。
2014年

5

如果要确保没有任务同时运行,请使用SingleThreadedExecutor。任务将按照提交顺序进行处理。您甚至不需要保留任务,只需将其提交给执行人员即可。


2

简单代码实现Callback机制使用ExecutorService

import java.util.concurrent.*;
import java.util.*;

public class CallBackDemo{
    public CallBackDemo(){
        System.out.println("creating service");
        ExecutorService service = Executors.newFixedThreadPool(5);

        try{
            for ( int i=0; i<5; i++){
                Callback callback = new Callback(i+1);
                MyCallable myCallable = new MyCallable((long)i+1,callback);
                Future<Long> future = service.submit(myCallable);
                //System.out.println("future status:"+future.get()+":"+future.isDone());
            }
        }catch(Exception err){
            err.printStackTrace();
        }
        service.shutdown();
    }
    public static void main(String args[]){
        CallBackDemo demo = new CallBackDemo();
    }
}
class MyCallable implements Callable<Long>{
    Long id = 0L;
    Callback callback;
    public MyCallable(Long val,Callback obj){
        this.id = val;
        this.callback = obj;
    }
    public Long call(){
        //Add your business logic
        System.out.println("Callable:"+id+":"+Thread.currentThread().getName());
        callback.callbackMethod();
        return id;
    }
}
class Callback {
    private int i;
    public Callback(int i){
        this.i = i;
    }
    public void callbackMethod(){
        System.out.println("Call back:"+i);
        // Add your business logic
    }
}

输出:

creating service
Callable:1:pool-1-thread-1
Call back:1
Callable:3:pool-1-thread-3
Callable:2:pool-1-thread-2
Call back:2
Callable:5:pool-1-thread-5
Call back:5
Call back:3
Callable:4:pool-1-thread-4
Call back:4

重要说明:

  1. 如果要按FIFO顺序依次处理任务,请替换newFixedThreadPool(5)newFixedThreadPool(1)
  2. 如果要在分析上一个任务的结果后处理下callback一个任务,只需取消注释以下行

    //System.out.println("future status:"+future.get()+":"+future.isDone());
  3. 您可以替换newFixedThreadPool()

    Executors.newCachedThreadPool()
    Executors.newWorkStealingPool()
    ThreadPoolExecutor

    取决于您的用例。

  4. 如果要异步处理回调方法

    一个。将共享传递ExecutorService or ThreadPoolExecutor给Callable任务

    b。将您的Callable方法转换为Callable/Runnable任务

    C。将回调任务推送到 ExecutorService or ThreadPoolExecutor


1

只是为了增加Matt的回答(这有所帮助),下面是一个更加充实的示例来说明回调的用法。

private static Primes primes = new Primes();

public static void main(String[] args) throws InterruptedException {
    getPrimeAsync((p) ->
        System.out.println("onPrimeListener; p=" + p));

    System.out.println("Adios mi amigito");
}
public interface OnPrimeListener {
    void onPrime(int prime);
}
public static void getPrimeAsync(OnPrimeListener listener) {
    CompletableFuture.supplyAsync(primes::getNextPrime)
        .thenApply((prime) -> {
            System.out.println("getPrimeAsync(); prime=" + prime);
            if (listener != null) {
                listener.onPrime(prime);
            }
            return prime;
        });
}

输出为:

    getPrimeAsync(); prime=241
    onPrimeListener; p=241
    Adios mi amigito

1

您可以使用Callable的实现,以便

public class MyAsyncCallable<V> implements Callable<V> {

    CallbackInterface ci;

    public MyAsyncCallable(CallbackInterface ci) {
        this.ci = ci;
    }

    public V call() throws Exception {

        System.out.println("Call of MyCallable invoked");
        System.out.println("Result = " + this.ci.doSomething(10, 20));
        return (V) "Good job";
    }
}

CallbackInterface是非常基本的东西

public interface CallbackInterface {
    public int doSomething(int a, int b);
}

现在主班级看起来像这样

ExecutorService ex = Executors.newFixedThreadPool(2);

MyAsyncCallable<String> mac = new MyAsyncCallable<String>((a, b) -> a + b);
ex.submit(mac);

1

这是对Pache使用番石榴的答案的扩展ListenableFuture

特别是,Futures.transform()返回ListenableFuture值可用于链接异步调用。Futures.addCallback()返回void,因此不能用于链接,但是对于在异步完成时处理成功/失败非常有用。

// ListenableFuture1: Open Database
ListenableFuture<Database> database = service.submit(() -> openDatabase());

// ListenableFuture2: Query Database for Cursor rows
ListenableFuture<Cursor> cursor =
    Futures.transform(database, database -> database.query(table, ...));

// ListenableFuture3: Convert Cursor rows to List<Foo>
ListenableFuture<List<Foo>> fooList =
    Futures.transform(cursor, cursor -> cursorToFooList(cursor));

// Final Callback: Handle the success/errors when final future completes
Futures.addCallback(fooList, new FutureCallback<List<Foo>>() {
  public void onSuccess(List<Foo> foos) {
    doSomethingWith(foos);
  }
  public void onFailure(Throwable thrown) {
    log.error(thrown);
  }
});

注意:除了链接异步任务,Futures.transform()还允许您将每个任务安排在单独的执行器上(此示例中未显示)。


这似乎很好。
kaiser
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.