1. Обзор
С первых дней существования Java многопоточность была важным аспектом языка. Runnable
— это основной интерфейс, предназначенный для представления многопоточных задач, а Java 1.5 предоставляет Callable
как улучшенную версию Runnable
.
В этом руководстве мы рассмотрим различия и области применения обоих интерфейсов.
2. Механизм исполнения
Оба интерфейса предназначены для представления задачи, которая может выполняться несколькими потоками. Мы можем запускать задачи Runnable
, используя класс Thread или
ExecutorService
, тогда как мы можем запускать Callable
только с помощью последнего.
3. Возвращаемые значения
Давайте подробнее рассмотрим, как эти интерфейсы обрабатывают возвращаемые значения.
3.1. С возможностью запуска
Интерфейс Runnable
является функциональным интерфейсом и имеет единственный метод run()
, который не принимает никаких параметров и не возвращает никаких значений.
Это работает для ситуаций, когда мы не ищем результат выполнения потока, например регистрацию входящих событий:
public interface Runnable {
public void run();
}
Давайте разберем это на примере:
public class EventLoggingTask implements Runnable{
private Logger logger
= LoggerFactory.getLogger(EventLoggingTask.class);
@Override
public void run() {
logger.info("Message");
}
}
В этом примере поток просто прочитает сообщение из очереди и зарегистрирует его в файле журнала. Задача не возвращает никакого значения.
Мы можем запустить задачу с помощью ExecutorService
:
public void executeTask() {
executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new EventLoggingTask());
executorService.shutdown();
}
В этом случае объект Future
не будет содержать никакого значения.
3.2. С возможностью вызова
Интерфейс Callable
— это универсальный интерфейс, содержащий единственный метод call()
, который возвращает универсальное значение V
:
public interface Callable<V> {
V call() throws Exception;
}
Рассмотрим вычисление факториала числа:
public class FactorialTask implements Callable<Integer> {
int number;
// standard constructors
public Integer call() throws InvalidParamaterException {
int fact = 1;
// ...
for(int count = number; count > 1; count--) {
fact = fact * count;
}
return fact;
}
}
Результат метода call()
возвращается в объекте Future :
@Test
public void whenTaskSubmitted_ThenFutureResultObtained(){
FactorialTask task = new FactorialTask(5);
Future<Integer> future = executorService.submit(task);
assertEquals(120, future.get().intValue());
}
4. Обработка исключений
Посмотрим, насколько они подходят для обработки исключений.
4.1. С возможностью запуска
Поскольку в сигнатуре метода не указана фраза throws, у нас нет способа распространять дополнительные проверенные исключения.
4.2. С возможностью вызова
`
Метод call()
Callable содержит предложение « throws
Exception` », поэтому мы можем легко распространять проверенные исключения дальше:
public class FactorialTask implements Callable<Integer> {
// ...
public Integer call() throws InvalidParamaterException {
if(number < 0) {
throw new InvalidParamaterException("Number should be positive");
}
// ...
}
}
В случае запуска Callable
с помощью ExecutorService
исключения собираются в объекте Future .
Мы можем проверить это, вызвав метод Future.get()
.
Это вызовет ExecutionException
, которое упаковывает исходное исключение:
@Test(expected = ExecutionException.class)
public void whenException_ThenCallableThrowsIt() {
FactorialCallableTask task = new FactorialCallableTask(-5);
Future<Integer> future = executorService.submit(task);
Integer result = future.get().intValue();
}
В приведенном выше тесте выдается исключение ExecutionException
, поскольку мы передаем недопустимое число. Мы можем вызвать метод getCause()
для этого объекта исключения, чтобы получить исходное проверенное исключение.
Если мы не вызовем метод get()
класса Future
, об исключении, сгенерированном методом call()
, не будет сообщено, и задача все равно будет помечена как выполненная:
@Test
public void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled(){
FactorialCallableTask task = new FactorialCallableTask(-5);
Future<Integer> future = executorService.submit(task);
assertEquals(false, future.isDone());
}
Приведенный выше тест пройдет успешно, несмотря на то, что мы создали исключение для отрицательных значений параметра в FactorialCallableTask
.
5. Вывод
В этой статье мы рассмотрели различия между интерфейсами Runnable
и Callable
.
Как всегда, полный код для этой статьи доступен на GitHub .