Запуск потоков в Java
1. Введение
В этой статье мы разберем различные способы запуска потоков. Описав задачу, мы выполнить ее в многопоточной среде при помощи класса Thread и сравним с продвинутыми способами работы создания и запуска потоков.
2. Базовый запуск Thread
2.1 Переопределить метод Thread.run
Описав простую задачу, мы можем ее выполнить в многопоточной среде при помощи класса Thread.
Java Virtual Machine (JVM) позволяет выполнять задачи одновременно благодаря классу Thread, что и определяет многопоточную среду. Для этого достаточно создать экземпляр данного класса, определить задачу и просто запустить поток.
Возьмем простой пример за основу.
public class ClockThread extends Thread {
public String startTime = "00:00";
public void run() {
StringBuilder sb = new StringBuilder(startTime);
for (int i = 0; i <= 5; i++) {
sb.replace(startTime.length() - 1, startTime.length(), String.valueOf(i));
System.out.println(sb);
}
}
}
Создав простой класс и унаследовав его от класса Thread мы получаем доступ к всем его публичным методам. Воспользовавшись наследованием, возможно переопределить метод run для определения собственной задачи выполняемой потоком.
Для запуска потока достаточно создать экземпляр класса и выполнить метод start.
public class Multithreading {
public static void main(String[] args) {
new ClockThread().start();
}
}
Результат выполнения:
00:01
00:02
00:03
00:04
00:05
Пример с запуском нескольких потоков будет выглядеть практически аналогично.
public class Multithreading {
public static void main(String[] args) {
new ClockThread().start();
new ClockThread().start();
}
}
2.2 Интерфейс Runnable
Мы уже познакомились с методом run, однако класс Thread не является владельцем данного метода, а всего лишь реализует его по контракту интерфейса Runnable.
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Интерфейс Runnable может быть реализован подразумевая, что реализация будет выполнена потоком, где основная задача метода run - выполнение кода до тех пор, пока поток активен.
Класс Thread уже реализовал метод run.
public class Thread {
...
@Override
public void run() {
if (target != null) {
target.run();
}
}
...
}
Из кода исходит, что метод run выполняет другой метод run, если экземпляр типа Runnable проинициализирован в свойстве target. В противном случае, поток после вызова метода start практически мгновенно завершит свое “холостое” выполнение.
Поскольку любой экземпляр класса Thread содержит свойство типа Runnable с именем target, переопределение метода run в рамках наследования становится более сложным и теряет смысл, в сравнении простой реализацией интерфейса.
Реализация:
public class ClockRunnable implements Runnable {
public String startTime = "00:00";
public void run() {
StringBuilder sb = new StringBuilder(startTime);
for (int i = 0; i <= 5; i++) {
sb.replace(startTime.length() - 1, startTime.length(), String.valueOf(i));
System.out.println(sb);
}
}
}
Инициализация:
public class Multithreading {
public static void main(String[] args) {
new Thread(new ClockRunnable()).start();
}
}
Перегруженный конструктор класса Thread позволяет инициализировать объект с одновременной передачей экземпляра типа Runnable в виде параметра, что впоследствии установит значение для свойства target в классе Thread.
Разницы между наследованием и реализацией с точки зрения кода практически нет, однако наследование подразумевает использование методов родителей, расширение, формирование иерархии классов и так далее.
Таким образом, реализация одного простого метода интерфейса Runnable гораздо проще и всегда может быть подменена другой реализаций в рамках уже самодостаточного класса Thread.
2.3 Лямбда-выражение
Интерфейс Runnable является функциональным интерфейсом, что позволяет применять лямбда-выражение и немного сократить часто повторяющейся код.
public class Multithreading {
public static void main(String[] args) {
String startTime = "00:00";
new Thread(() -> {
StringBuilder sb = new StringBuilder(startTime);
for (int i = 0; i <= 5; i++) {
sb.replace(startTime.length() - 1, startTime.length(), String.valueOf(i));
System.out.println(sb);
}
}).start();
}
}
3. Executors
В свое время появление Java 5 принесло множество нововведений, которые до сих не перестают нас удивлять и применяются повсеместно. Одним из таких нововведений стал пакет java.unit.concurrent, содержащий класс-фабрику Executors.
Класс Executors является фабрикой для создания различных реализаций интерфейса ExecutorService.
Основные задачи ExecutorService:
- Управление запуском и уничтожения потоков
- Создание объектов типа данных Future для работы с результатом выполнения асинхронных операций
public class Multithreading {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(1);
String startTime = "00:00";
executorService.execute(() -> {
StringBuilder sb = new StringBuilder(startTime);
for (int i = 0; i <= 5; i++) {
sb.replace(startTime.length() - 1, startTime.length(), String.valueOf(i));
System.out.println(sb);
}
});
executorService.shutdown();
}
}
Запуск потока, но уже с возвращением результата после его выполнения будет выглядеть следующим образом. Это обеспечивается замещением реализации интерфейса Runnable на реализацию интерфейса Callable и вызовом метода submit из интерфейса ExecutorService.
public class Multithreading {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(1);
String startTime = "00:00";
Future<StringBuilder> result = executorService.submit(() -> {
StringBuilder sb = new StringBuilder(startTime);
for (int i = 0; i <= 5; i++) {
sb.replace(startTime.length() - 1, startTime.length(), String.valueOf(i));
System.out.println(sb);
}
return sb;
});
executorService.shutdown();
}
}
Для запуска нескольких потоков будет достаточно создать один экземпляр ExecutorService и вызвать метод invokeAll c передачей коллекции объектов типа данных Callable.
public class Multithreading {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
List<Callable<String>> callables = List.of(
() -> "00:01",
() -> "00:02",
() -> "00:03",
() -> "00:04",
() -> "00:05",
});
List<Future<String>> callables = executorService.invokeAll(callables);
executorService.shutdown();
}
}
Примечательно, что интерфейсы Callable и Runnable созданы для одинаковой цели - быть выполненным потоком. Но есть одно различие - Callable возвращает результат после выполнения метода call, а Runnable после выполнение метода run - нет.
Заключение
Java предлагает разнообразные способы запуска потоков, от простого использования интерфейса Runnable в сопряжении с классом Thread и до интерфейса Callable когда необходимо получать результат выполнения асинхронной операции.
ЕxecutorService может быть очень полезным, если необходимо управлять большим количеством потоков и взаимодействовать с результатом их выполнения, в сравнении с традиционным запусков потоков через метод start класса Thread.