Запуск потоков в Java

Запуск потоков в 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.

источник


Report Page