В этом руководстве по параллелизму Java мы изучим интерфейсы Runnable и Callable с практическими примерами. Мы также узнаем несколько основных различий между двумя интерфейсами и как выбрать один из них в многопоточном приложении.
1. Введение
1.1.Рабочий интерфейс
Runnable — это базовый интерфейс, и мы можем выполнить его реализующие экземпляры как Thread или отправить в ExecutorService. Этот интерфейс содержит только один абстрактный метод run(), который мы должны переопределить, чтобы определить задание Thread. Начиная с Java 8, Runnable — это функциональный интерфейс.
@FunctionalInterfacepublic interface Runnable {public abstract void run();}
1.2 Вызываемый интерфейс
Callable также является одним из основных интерфейсов, и они могут быть выполнены только через ExecutorService, а не традиционным классом Thread. Он содержит один абстрактный метод call(), который должен содержать бизнес-логику, которая должна быть выполнена ExecutorService. Callable также является функциональным интерфейсом.
@FunctionalInterfacepublic interface Callable<V> {V call() throws Exception;}
2. Различия между Runnable и Callable
Давайте теперь рассмотрим некоторые основные различия между двумя интерфейсами:
2.1 Методы переопределения
Чтобы использовать интерфейс Runnable, нам необходимо переопределить метод run().
class CallableTask implements Callable<String>{public String call() throws Exception{return "Returning from callable";}}
Чтобы использовать интерфейс Callable, нам необходимо переопределить метод call().
class RunnableTask implements Runnable {public void run() {System.out.println("Thread executed !");}}
2.2 Механизм исполнения
Интерфейсы Callable и Runnable используются для инкапсуляции задач, которые должны быть выполнены другим потоком.
Запускаемые экземпляры могут быть запущены как классом Thread, так и ExecutorService.
RunnableTask task = new RunnableTask();Thread thread = new Thread(task);thread.start();
Вызываемые экземпляры могут быть выполнены только через ExecutorService. Ни один конструктор, определенный в классе Thread, не принимает вызываемый экземпляр.
ExecutorService executor = Executors.newFixedThreadPool(2);CallableTask task = new CallableTask();Future<String> future = executor.submit(task);
2.3 Типы возвращаемых данных
В Runnable возвращаемый тип run() — void, поэтому этот метод не может возвращать никаких значений.
public void run();
В Callable метод call() возвращает объект Future, который предоставляет методы для получения результата вычисления и проверки того, была ли задача завершена или отменена.
public Object call() throws Exception;
2.4 Проверенные исключения
В Runnable метод run() не может выдавать проверенные исключения, поэтому у нас нет способа распространять проверенные исключения. Мы должны обрабатывать проверенные исключения внутри run(), используя только блок try/catch.
В следующем примере конструктор FileInputStream выдает проверенное исключение FileNotFoundException. Мы должны обработать исключение в блоке catch, иначе код не скомпилируется.
class FileReaderTask implements Runnable {public void run(){try(FileInputStream fis = new FileInputStream("file-path")){//read file} catch(FileNotFoundException e) {//handle exception} catch(IOException e) {//handle exception}}}
В Callable метод call() может выбрасывать проверенные исключения, которые мы можем легко распространять. В следующем примере мы можем позволить методу call() повторно выбрасывать исключение в метод caller для его обработки.
class FileReaderTask implements Callable {public Object call() throws IOException {try(FileInputStream fis = new FileInputStream("file-path")){//read file}return null;}}
На стороне метода вызывающего объекта ExecutionException выбрасывается, когда мы вызываем метод Future.get(). Если мы не вызываем метод get(), исключение будет потеряно, а поток будет помечен как завершенный.
Future<Integer> future = executorService.submit(callableTask); //an exception is thrown from callable taskfuture.isDone(); //true - no exception reportedfuture.get()...; //throws ExecutionException
3. Когда использовать
Интерфейсы Runnable и Callable имеют свои применения, и фреймворк Executor в Java поддерживает оба. Runnable существует уже давно, но он все еще используется и является основным интерфейсом для проектирования параллельных приложений.
- Runnable не возвращает никакого значения, поэтому мы можем использовать его для вызовов по принципу «запустил и забыл», особенно когда нас не интересует результат выполнения задачи.
- Вызываемые методы могут создавать исключения и возвращать значения, поэтому они лучше подходят для задач, предполагающих получение результата(например, извлечение ресурса из сети, выполнение дорогостоящих вычислений для получения некоторого значения и т. д.).
4. Заключение
Мы узнали о Java Runnable и Callable Interfaces с примерами. Мы также увидели некоторые из основных различий между ними и то, как решить, какой из них выбрать при работе в многопоточной среде.