Разница между интерфейсами Runnable и Callable в Java
Разница между Runnable
и Callable
в Java при проектировании многопоточности
Я работаю над проектом на Java, в котором требуется реализовать многопоточность, и столкнулся с выбором между использованием интерфейсов Runnable
и Callable
. Можете пояснить, в чем основные различия между этими двумя подходами и в каких случаях следует использовать каждый из них? Какие преимущества и недостатки у каждого из методов?
5 ответ(ов)
В Java Callable
и Runnable
являются функциональными интерфейсами, но у них есть несколько ключевых отличий:
Метод:
Callable
должен реализовать методcall()
, тогда какRunnable
реализует методrun()
.public interface Runnable { void run(); } public interface Callable<V> { V call() throws Exception; }
Возврат значения:
Callable
может возвращать значение при выполнении, в отличие отRunnable
, который не возвращает никакого значения. Это означает, что вы можете использоватьCallable
для получения результата выполнения:Callable<Integer> callableTask = () -> { // Некоторая логика return 42; };
Обработка исключений:
Callable
может выбрасывать проверенные исключения (checked exceptions), тогда как вRunnable
это не предусмотрено. Это позволяетCallable
обрабатывать более сложные ситуации:Callable<Void> callableTask = () -> { // Логика, которая может выбросить исключение throw new IOException("Ошибка ввода/вывода"); };
Использование с
ExecutorService
:Callable
может быть использован с методамиExecutorService#invokeXXX(Collection<? extends Callable<T>> tasks)
, в то время какRunnable
не может быть использован в этом контексте. Это связано с тем, чтоinvokeXXX
возвращает результат выполненияCallable
задач.
Таким образом, если вам необходимо выполнять параллельные задачи, которые возвращают результат и могут выбрасывать исключения, вам стоит использовать Callable
. В противном случае, если результат не важен и не ожидаются исключения, вы можете использовать Runnable
.
Давайте рассмотрим, где следует использовать интерфейсы Runnable
и Callable
.
Оба интерфейса выполняются в другом потоке, отличном от вызывающего, однако Callable
может возвращать значение, тогда как Runnable
- нет. Где же это действительно имеет значение?
Runnable: Если у вас есть задача, которую нужно просто выполнить и больше не беспокоиться об этом, используйте Runnable
. Заключите ваш код в Runnable
, и когда будет вызван метод run()
, ваше задание будет выполнено. Вызывающий поток не заботится о том, когда будет выполнена ваша задача.
Callable: Если вам нужно получить значение из задачи, используйте Callable
. Но сам по себе Callable
не выполнит задачу. Вам понадобится объект Future
, который вы обернете вокруг вашего Callable
, чтобы получить значения при вызове future.get()
. В этом случае вызывающий поток будет заблокирован до тех пор, пока Future
не вернет результаты, а это может произойти только после выполнения метода call()
вашего Callable
.
Рассмотрите ситуацию с интерфейсом для целевого класса, в котором определены методы, оборачивающие как Runnable
, так и Callable
. Вызывающий класс будет случайным образом вызывать методы вашего интерфейса, не зная, какой из них является Runnable
, а какой Callable
. Методы Runnable
будут выполняться асинхронно, пока не будет вызван метод Callable
. В этом случае поток вызывающего класса будет заблокирован, поскольку вы извлекаете значения из целевого класса.
Примечание: Внутри вашего целевого класса можно вызывать Callable
и Runnable
на одном исполнительном потоке, что делает этот механизм похожим на последовательную очередь обработки. Таким образом, пока вызывающий класс вызывает ваши методы, обернутые в Runnable
, поток вызывающего класса будет выполняться очень быстро без блокировки. Как только он вызовет метод, обернутый в Callable
, ему придется заблокироваться, пока все другие элементы в очереди не будут выполнены. Только после этого метод вернет значения. Это механизм синхронизации.
Интерфейс Callable
объявляет метод call()
, который должен возвращать результат с указанным типом с помощью дженериков. Вот как это выглядит:
public interface Callable<V> {
/**
* Вычисляет результат или выбрасывает исключение, если это невозможно.
*
* @return вычисленный результат
* @throws Exception если невозможно вычислить результат
*/
V call() throws Exception;
}
С другой стороны, Runnable
— это интерфейс, который объявляет метод run()
, который вызывается при создании потока с использованием реализации этого интерфейса и вызове метода start()
. Вы также можете напрямую вызвать метод run()
, но в этом случае он просто выполнится в том же потоке.
public interface Runnable {
/**
* Когда объект, реализующий интерфейс <code>Runnable</code>, используется
* для создания потока, запуск этого потока вызывает метод
* <code>run</code> этого объекта в отдельно выполняющемся
* потоке.
* <p>
* Общий контракт метода <code>run</code> заключается в том, что он может
* выполнять любые действия.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
Вкратце, некоторые заметные отличия между этими интерфейсами:
- Объект
Runnable
не возвращает результат, в то время как объектCallable
возвращает результат. - Объект
Runnable
не может выбрасывать проверяемые исключения, тогда как объектCallable
может выбрасывать исключения. - Интерфейс
Runnable
существует с Java 1.0, тогда какCallable
был введен только в Java 1.5.
Некоторые сходства включают:
- Экземпляры классов, реализующих интерфейсы
Runnable
илиCallable
, могут быть выполнены в другом потоке. - Экземпляры обоих интерфейсов
Callable
иRunnable
могут быть выполнены с помощьюExecutorService
через методsubmit()
. - Оба являются функциональными интерфейсами и могут быть использованы в лямбда-выражениях, начиная с Java 8.
Методы интерфейса ExecutorService
выглядят следующим образом:
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
Как уже упоминалось, интерфейс Callable является относительно новым и был введен в рамках пакета для работы с параллелизмом. И Callable, и Runnable могут использоваться с исполнителями (executors). Класс Thread (который сам реализует интерфейс Runnable) поддерживает только Runnable.
Тем не менее, вы все равно можете использовать Runnable с исполнителями. Преимущество Callable в том, что вы можете передать его исполнителю и сразу получить обратно объект Future, который будет обновлен, когда выполнение завершится. То же самое можно реализовать с помощью Runnable, но в этом случае вам придется самостоятельно управлять результатами. Например, вы можете создать очередь для результатов, которая будет хранить все результаты. Другой поток может ждать в этой очереди и обрабатывать поступившие результаты.
Разница между Callable
и Runnable
следующие:
Callable
был введен в JDK 5.0, в то время какRunnable
существует с JDK 1.0.- У
Callable
есть методcall()
, а уRunnable
— методrun()
. - Метод
call
возвращает значение, тогда как методrun
ничего не возвращает. - Метод
call
может выбрасывать проверяемые исключения, в то время как методrun
не может этого сделать. - Для добавления задачи в очередь
Callable
использует методsubmit()
, аRunnable
— методexecute()
.
"implements Runnable" против "extends Thread" в Java: что выбрать?
Как работают сервлеты? Инстанцирование, сессии, общие переменные и многопоточность
Что значит 'synchronized'?
Почему методы wait() и notify() объявлены в классе Object в Java?
Когда следует использовать поток Java вместо Executor?