Puisque use ExecutorService
peut submit
une tâche Callable
et retourne Future
, pourquoi faut-il utiliser FutureTask
pour envelopper Callable
tâche et utiliser la méthode execute
? Je sens qu'ils font tous les deux la même chose.
En fait, vous avez raison. Les deux approches sont identiques. Vous n'avez généralement pas besoin de les emballer vous-même. Si vous l'êtes, vous dupliquez probablement le code dans AbstractExecutorService:
/**
* Returns a <tt>RunnableFuture</tt> for the given callable task.
*
* @param callable the callable task being wrapped
* @return a <tt>RunnableFuture</tt> which when run will call the
* underlying callable and which, as a <tt>Future</tt>, will yield
* the callable's result as its result and provide for
* cancellation of the underlying task.
* @since 1.6
*/
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
La seule différence entre Future et RunnableFuture, est la méthode run ():
/**
* A {@link Future} that is {@link Runnable}. Successful execution of
* the <tt>run</tt> method causes completion of the <tt>Future</tt>
* and allows access to its results.
* @see FutureTask
* @see Executor
* @since 1.6
* @author Doug Lea
* @param <V> The result type returned by this Future's <tt>get</tt> method
*/
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
Une bonne raison de laisser l'exécuteur construire la FutureTask pour vous est de vous assurer qu'il n'existe aucun moyen possible qu'il existe plusieurs références à l'instance de FutureTask. Autrement dit, l'exécuteur possède cette instance.
FutureTask Cette classe fournit un base implementation of Future
, avec des méthodes pour démarrer et annuler un calcul
Future est l'interface
Future
n'est qu'une interface. En coulisse, l'implémentation est FutureTask
.
Vous pouvez absolument utiliser FutureTask
manuellement mais vous perdrez les avantages d'utiliser Executor
(regroupement de threads, limitation du thread, etc.). L'utilisation de FutureTask
est assez similaire à l'utilisation de l'ancienne Thread
et à l'utilisation de la méthode run.
Vous n'auriez besoin d'utiliser FutureTask que si vous souhaitez modifier son comportement ou accéder à son Callable ultérieurement. Pour 99% des utilisations, utilisez simplement Callable et Future.
Comme Mark et d'autres ont répondu correctement que Future
est l'interface pour FutureTask
et Executor
efficacement son usine; ce qui signifie que le code d'application instancie rarement FutureTask
directement. Pour compléter la discussion, je fournis un exemple montrant une situation où FutureTask
est construit et utilisé directement, en dehors de tout Executor
:
FutureTask<Integer> task = new FutureTask<Integer>(()-> {
System.out.println("Pretend that something complicated is computed");
Thread.sleep(1000);
return 42;
});
Thread t1 = new Thread(()->{
try {
int r = task.get();
System.out.println("Result is " + r);
} catch (InterruptedException | ExecutionException e) {}
});
Thread t2 = new Thread(()->{
try {
int r = task.get();
System.out.println("Result is " + r);
} catch (InterruptedException | ExecutionException e) {}
});
Thread t3 = new Thread(()->{
try {
int r = task.get();
System.out.println("Result is " + r);
} catch (InterruptedException | ExecutionException e) {}
});
System.out.println("Several threads are going to wait until computations is ready");
t1.start();
t2.start();
t3.start();
task.run(); // let the main thread to compute the value
Ici, FutureTask
est utilisé comme outil de synchronisation, comme CountdownLatch
ou une primitive barrière similaire. Il aurait pu être réimplémenté en utilisant CountdownLatch
ou des verrous et des conditions; FutureTask
le rend simplement bien encapsulé, explicite, élégant et avec moins de code.
Notez également que la méthode FutureTask # run () doit être appelée explicitement, dans l'un des threads; il n'y a pas d'exécuteur pour le faire pour vous. Dans mon code, il est éventuellement exécuté par le thread principal, mais on peut modifier la méthode get()
pour appeler run()
sur le premier thread appelant get()
, donc le premier thread atteignant get()
, et il peut s'agir de T1, T2 ou T3, ferait le calcul pour tous les threads restants.
Sur cette idée - le premier thread demandant le résultat ferait le calcul pour les autres, tandis que les tentatives simultanées seraient bloquées - est basé Memoizer, voir l'exemple Memoizer Cache de la page 108 dans "Java Concurrency in Practice".