Dites que j'ai une file d'attente pleine de tâches que je dois soumettre à un service d'exécution. Je veux qu'ils soient traités un à la fois. Le moyen le plus simple auquel je puisse penser est de:
Cependant, j'essaie d'éviter de bloquer complètement. Si j'ai 10 000 files d'attente de ce type, dont les tâches doivent être traitées une à la fois, je manquerai d'espace dans la pile, car la plupart d'entre elles conserveront des threads bloqués.
Ce que je voudrais, c'est soumettre une tâche et fournir un rappel qui est appelé lorsque la tâche est terminée. J'utiliserai cette notification de rappel comme indicateur pour envoyer la tâche suivante. (functionaljava et jetlang utilisent apparemment de tels algorithmes, mais je ne comprends pas leur code)
Comment puis-je faire cela en utilisant Java.util.concurrent de JDK, à moins d'écrire mon propre service exécuteur?
(la file d'attente qui me nourrit ces tâches peut elle-même bloquer, mais c'est un problème à résoudre plus tard)
Définissez une interface de rappel pour recevoir les paramètres que vous souhaitez transmettre dans la notification d'achèvement. Puis appelez-le à la fin de la tâche.
Vous pouvez même écrire un wrapper général pour les tâches Runnable et les soumettre à ExecutorService
. Ou, voir ci-dessous un mécanisme intégré à 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();
}
}
Avec CompletableFuture
, Java 8 incluait un moyen plus élaboré de composer des pipelines dans lesquels les processus peuvent être terminés de manière asynchrone et conditionnelle. Voici un exemple de notification artificiel mais complet.
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()));
}
}
En Java 8, vous pouvez utiliser CompletableFuture . Voici un exemple dans mon code où je l'utilise pour extraire des utilisateurs de mon service utilisateur, les mapper sur mes objets de vue, puis mettre à jour ma vue ou afficher une boîte de dialogue d'erreur (il s'agit d'une application à interface graphique):
CompletableFuture.supplyAsync(
userService::listUsers
).thenApply(
this::mapUsersToUserViews
).thenAccept(
this::updateView
).exceptionally(
throwable -> { showErrorDialogFor(throwable); return null; }
);
Il s'exécute de manière asynchrone. J'utilise deux méthodes privées: mapUsersToUserViews
et updateView
.
Utilisez la future API écoutable de Guava et ajoutez un rappel. Cf. sur le site:
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!
}
});
Vous pouvez étendre la classe FutureTask
et remplacer la méthode done()
, puis ajouter l'objet FutureTask
à la ExecutorService
, de sorte que la méthode done()
sera appelée lorsque la FutureTask
sera terminée immédiatement.
ThreadPoolExecutor
possède également des méthodes de raccordement beforeExecute
et afterExecute
que vous pouvez remplacer et utiliser. Voici la description de ThreadPoolExecutor
's Javadocs .
Méthodes de crochet
Cette classe fournit les méthodes
beforeExecute(Java.lang.Thread, Java.lang.Runnable)
etafterExecute(Java.lang.Runnable, Java.lang.Throwable)
qui sont protégées et qui sont appelées avant et après l'exécution de chaque tâche. Ceux-ci peuvent être utilisés pour manipuler l'environnement d'exécution; par exemple, réinitialiserThreadLocals
, rassembler des statistiques ou ajouter des entrées de journal. En outre, la méthodeterminated()
peut être remplacée pour effectuer tout traitement spécial à effectuer une fois que la variableExecutor
est complètement terminée. Si les méthodes de raccordement ou de rappel génèrent des exceptions, les threads de travail internes peuvent à leur tour échouer et se terminer brutalement.
Utilisez un CountDownLatch
.
C'est à partir de Java.util.concurrent
et c'est exactement le moyen d'attendre l'exécution de plusieurs threads avant de continuer.
Pour obtenir l'effet de rappel que vous recherchez, cela nécessite un peu de travail supplémentaire. C'est-à-dire que vous gérez cela par vous-même dans un thread séparé qui utilise la variable CountDownLatch
et l'attend, puis continue à notifier tout ce que vous devez notifier. Il n'y a pas de support natif pour le rappel, ou quelque chose de similaire à cet effet.
EDIT: maintenant que je comprends mieux votre question, je pense que vous allez trop loin, inutilement. Si vous prenez un SingleThreadExecutor
normal, donnez-lui toutes les tâches, et la mise en file d'attente se fera de manière native.
Si vous voulez vous assurer qu'aucune tâche ne s'exécutera en même temps, utilisez un SingleThreadedExecutor . Les tâches seront traitées dans l'ordre dans lequel elles sont soumises. Vous n'avez même pas besoin de garder les tâches, il vous suffit de les soumettre à l'exécutif.
Code simple pour implémenter le mécanisme Callback
à l'aide de 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
}
}
sortie:
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
Notes clés:
newFixedThreadPool(5)
par newFixedThreadPool(1)
. Si vous souhaitez traiter la tâche suivante après avoir analysé le résultat de callback
de la tâche précédente, supprimez simplement le commentaire sous la ligne.
//System.out.println("future status:"+future.get()+":"+future.isDone());
Vous pouvez remplacer newFixedThreadPool()
par l’un des
Executors.newCachedThreadPool()
Executors.newWorkStealingPool()
ThreadPoolExecutor
en fonction de votre cas d'utilisation.
Si vous voulez gérer la méthode de rappel de manière asynchrone
une. Passer un ExecutorService or ThreadPoolExecutor
partagé à la tâche appelable
b. Convertissez votre méthode Callable
en tâche Callable/Runnable
c. Pousser la tâche de rappel à ExecutorService or ThreadPoolExecutor
Ceci est une extension de la réponse de Pache en utilisant la variable ListenableFuture
de Guava.
En particulier, Futures.transform()
renvoie ListenableFuture
et peut donc être utilisé pour chaîner des appels asynchrones. Futures.addCallback()
renvoie void
, ne peut donc pas être utilisé pour le chaînage, mais convient pour gérer le succès/l'échec d'une exécution asynchrone.
// 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);
}
});
REMARQUE: Outre le chaînage de tâches asynchrones, Futures.transform()
vous permet également de planifier chaque tâche sur un exécuteur distinct (non illustré dans cet exemple).
Pour ajouter à la réponse de Matt, ce qui a aidé, voici un exemple plus étoffé pour montrer l'utilisation d'un rappel.
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;
});
}
La sortie est:
getPrimeAsync(); prime=241
onPrimeListener; p=241
Adios mi amigito
Vous pouvez utiliser une implémentation de Callable telle que
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";
}
}
où CallbackInterface est quelque chose de très basique comme
public interface CallbackInterface {
public int doSomething(int a, int b);
}
et maintenant la classe principale ressemblera à ceci
ExecutorService ex = Executors.newFixedThreadPool(2);
MyAsyncCallable<String> mac = new MyAsyncCallable<String>((a, b) -> a + b);
ex.submit(mac);