В Java существует несколько способов выполнить код/задачу в отдельном потоке.
Наследование класса Thread
Первый способ – это создать свой класс, который наследуется от класса Thread.
Например,
public void run() {
…..
}
}
Для запуска нашего потока нужно создать инстанс класса и вызвать метод start():
myTread.start();
Также можно создать анонимный класс, наследующий класс Thread:
public void run() {
…..
}
}
myThread.start();
Имплиментировать интерфейс Runnable
Для этого нужно объявить класс, который реализует интерфейс Runnablе. Далее создать инстанс этого класса и передать его в конструктор объекта класса Thread и вызвать метод start():
public void run() {
…..
}
}
….
Runnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
Аналогично, вместо явного объявления класса, который реализует интерфейс Runnable, можно создать анонимный класс:
public void run(){
…..
}
};
…
Thread thread = new Thread(myRunnable);
thread.start();
Или используя lambda:
…
Thread thread = new Thread(myRunnable);
thread.start();
Использование Executor
Вместо явного создания потока, задачу можно выполнить используя Executor framework. Для этого нужно создать Thread Pool:
И вызвать метод execute, в который нужно передать наш Runnable:
public void run() {
…..
}
};
executor.execute(myRunnable);
Существует четыре основных Thread Pool, которые можно использовать:
newFixedThreadPool – создает новые потоки, по мере сабмита тасок, вплоть до максимального размера пула. Далее поддерживает размер пула постоянным. Если поток упадет из-за unexpected Exception, то создаст новый поток.
newCachedThreadPool – Если потоки не используются(idle), может их убивать. Если же число задач увеличивается, то создает новые потоки. При этом не имеет верхнего предела по числу потоков.
newSingleThreadExcutor – создает всего один поток. Если он падает, то создает новый. Гарантирует выполнение задач последовательно.
newScheduledThreadPool – Пул фиксированного размера. Поддерживает выполение отложенных и периодических задач по рассписанию.
Callable, Future и ExecutorService
Как вы успели заметить, Runnable имеет один метод – run, который не возращает никакого результата (void). Если нам надо, чтобы наша задача/код, выполняемая в отдельном потоке, вернула какой-то результат – мы можем использовать Callable.
Объявим класс (анонимный), который реализует Callable:
public List<String> call() throws Exception {
……..
return result;
}
};
Создадим ExecutorService и вызовем метод submit, вместо execute:
Future<List<String>> future = executor.submit(myCallable);
try {
List<String> result = future.get();
} catch (InterruptedException e) {
….
} catch (ExecutionException e) {
….
}
Метод submit вернет в качестве результата Future. Для получения результата, нужно вызвать метод get. Этот метод блокирующий, вызывающий поток будет ожидать, потока результат станет доступным.
CompletionService
Если мы хотим выполнить множество задач, и результаты получать в порядке их доступности, то можно использовать CompletionService.
Для этого нужно обернуть ExecutorService в ExecutorCompletionService:
CompletionService<List<String>> completionService = new ExecutorCompletionService<>(executor);
for (….){
completionService.submit(myCallable);
}
for (…){
try {
Future<List<String>> future = completionService.take();
List<String> result = future.get();
} catch (InterruptedException e) {
…..
} catch (ExecutionException e) {
……
}
}
С будущих статьях также расскажу про CompletableFuture и виртуальные потоки.