Так как использовать ExecutorService
можно submit
a Callable
задачу и вернуть Future
, зачем нужно использовать FutureTask
для переноса задачи Callable
и использовать метод execute
? Я чувствую, что они оба делают то же самое.
Какая разница между Future и FutureTask в Java?
Ответ 1
На самом деле вы правы. Оба подхода идентичны. Обычно вам не нужно их обертывать. Если да, вы, вероятно, дублируете код в 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 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);
}
Единственное различие между Future и RunnableFuture - это метод 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 <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();
}
Повод, чтобы позволить Исполнителю построить FutureTask для вас, состоит в том, чтобы гарантировать, что в экземпляр FutureTask не существует более одной ссылки. То есть, Исполнитель имеет этот экземпляр.
Ответ 2
FutureTask
Этот класс предоставляет base implementation of Future
, с помощью методов запуска и отмены вычисления
Future - это интерфейс
Ответ 3
Future
- это просто интерфейс. За сценой реализация FutureTask
.
Вы можете использовать FutureTask
вручную, но вы потеряете преимущества использования Executor
(объединение потока, ограничение потока и т.д.). Использование FutureTask
довольно похоже на использование старого Thread
и использование метода run.
Ответ 4
Вам нужно будет использовать FutureTask, если вы хотите изменить его поведение или позже получить доступ к его Callable. Для 99% использования просто используйте Callable и Future.
Ответ 5
Как и Mark и другие, правильно ответил, что Future
- это интерфейс для FutureTask
и Executor
эффективно его factory; что код приложения редко реализует FutureTask
напрямую. В дополнение к обсуждению я предоставляю пример, показывающий ситуацию, когда FutureTask
создается и используется непосредственно, вне любого 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
Здесь FutureTask
используется как инструмент синхронизации, например CountdownLatch
или аналогичный барьерный примитив. Он мог быть реализован с помощью CountdownLatch
или блокировок и условий; FutureTask
просто делает его красиво инкапсулированным, понятным, элегантным и с меньшим количеством кода.
Также обратите внимание, что метод FutureTask # run() должен быть вызван явно в любом из потоков; там нет Исполнителя, чтобы сделать это за вас. В моем коде он в конечном итоге выполняется основным потоком, но можно изменить метод get()
для вызова run()
в первом потоке, вызывающем get()
, поэтому первый поток достигает get()
, и он может быть любым из T1, T2 или T3, выполнит расчет для всех остальных потоков.
В этой идее - первый запрос на запрос потока будет выполнять вычисления для других, тогда как одновременные попытки будут заблокированы - основан на Memoizer, см. пример кэша Memoizer на стр. 108 в "Java Concurrency на практике".