Как начать тему в Java

Как начать поток в Java

1. Вступление

В этом руководстве мы собираемся изучить различные способы запуска потока и выполнения параллельных задач.

This is very useful, in particular when dealing with long or recurring operations that can’t run on the main thread, или когда взаимодействие с пользовательским интерфейсом нельзя приостановить, ожидая результатов операции.

Чтобы узнать больше о деталях потоков, обязательно прочитайте наш учебник оLife Cycle of a Thread in Java.

2. Основы запуска потока

Мы можем легко написать некоторую логику, которая работает в параллельном потоке, используя структуруThread.

Давайте попробуем простой пример, расширив классThread:

public class NewThread extends Thread {
    public void run() {
        long startTime = System.currentTimeMillis();
        int i = 0;
        while (true) {
            System.out.println(this.getName() + ": New Thread is running..." + i++);
            try {
                //Wait for one sec so it doesn't print too fast
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ...
        }
    }
}

А теперь мы пишем второй класс для инициализации и запуска нашего потока:

public class SingleThreadExample {
    public static void main(String[] args) {
        NewThread t = new NewThread();
        t.start();
    }
}

Теперь предположим, что нам нужно запустить несколько потоков:

public class MultipleThreadsExample {
    public static void main(String[] args) {
        NewThread t1 = new NewThread();
        t1.setName("MyThread-1");
        NewThread t2 = new NewThread();
        t2.setName("MyThread-2");
        t1.start();
        t2.start();
    }
}

Наш код все еще выглядит довольно просто и очень похож на примеры, которые мы можем найти в Интернете.

Конечно,this is far from production-ready code, where it’s of critical importance to manage resources in the correct way, to avoid too much context switching or too much memory usage.

So, to get production-ready we now need to write additional boilerplate, чтобы иметь дело с:

  • последовательное создание новых тем

  • количество одновременных живых потоков

  • освобождение потоков: очень важно для потоков демонов, чтобы избежать утечек

Если мы хотим, мы можем написать свой собственный код для всех этих сценариев и даже еще больше, но зачем нам изобретать велосипед?

3. ФреймворкExecutorService

ExecutorService реализует шаблон проектирования пула потоков (также называемый реплицированной моделью рабочего или рабочей группы) и заботится об управлении потоками, о котором мы упоминали выше, а также добавляет некоторые очень полезные функции, такие как повторное использование потоков и очереди задач.

В частности, очень важна возможность многократного использования потоков: в крупномасштабном приложении выделение и освобождение множества объектов потока создает значительные накладные расходы на управление памятью.

С помощью рабочих потоков мы минимизируем накладные расходы, связанные с созданием потоков.

Чтобы упростить настройку пула,ExecutorService поставляется с простым конструктором и некоторыми параметрами настройки, такими как тип очереди, минимальное и максимальное количество потоков и их соглашение об именах.

Для получения более подробной информации оExecutorService, прочтите нашGuide to the Java ExecutorService.

4. Начало задания с исполнителями

Благодаря этой мощной структуре мы можем переключить наше мышление с запуска потоков на отправку задач.

Давайте посмотрим, как мы можем отправить асинхронную задачу нашему исполнителю:

ExecutorService executor = Executors.newFixedThreadPool(10);
...
executor.submit(() -> {
    new Task();
});

Мы можем использовать два метода:execute, который ничего не возвращает, иsubmit, который возвращаетFuture, инкапсулирующий результат вычисления.

Для получения дополнительной информации оFutures, прочтите нашGuide to java.util.concurrent.Future.

5. Запуск задачи сCompletableFutures

Чтобы получить окончательный результат от объектаFuture, мы можем использовать методget, доступный в объекте, но это заблокирует родительский поток до конца вычислений.

В качестве альтернативы, мы могли бы избежать блока, добавив больше логики в нашу задачу, но мы должны увеличить сложность нашего кода.

Java 1.8 представила новую структуру поверх конструкцииFuture, чтобы лучше работать с результатом вычислений:CompletableFuture.

CompletableFuture реализуетCompletableStage, который добавляет широкий выбор методов для присоединения обратных вызовов и позволяет избежать всех подключений, необходимых для выполнения операций с результатом после его готовности.

Реализация для отправки задачи намного проще:

CompletableFuture.supplyAsync(() -> "Hello");

supplyAsync принимаетSupplier, содержащий код, который мы хотим выполнить асинхронно - в нашем случае параметр лямбда.

Теперь задача неявно передается вForkJoinPool.commonPool(), или мы можем указатьExecutor, который мы предпочитаем, в качестве второго параметра.

Чтобы узнать больше оCompletableFuture,, прочтите нашGuide To CompletableFuture.

6. Запуск отложенных или периодических задач

При работе со сложными веб-приложениями нам может потребоваться запускать задачи в определенное время, возможно, регулярно.

В Java есть несколько инструментов, которые могут помочь нам выполнять отложенные или повторяющиеся операции:

  • java.util.Timer

  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. Timer

Timer - это средство для планирования задач для будущего выполнения в фоновом потоке.

Задачи могут быть запланированы для одноразового выполнения или для повторного выполнения с регулярными интервалами.

Давайте посмотрим, как выглядит код, если мы хотим запустить задачу после задержки в одну секунду:

TimerTask task = new TimerTask() {
    public void run() {
        System.out.println("Task performed on: " + new Date() + "n"
          + "Thread's name: " + Thread.currentThread().getName());
    }
};
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);

А теперь давайте добавим повторяющееся расписание:

timer.scheduleAtFixedRate(repeatedTask, delay, period);

На этот раз задача будет запущена после указанной задержки и будет повторяться по истечении определенного периода времени.

Для получения дополнительной информации прочтите наше руководство поJava Timer.

6.2. ScheduledThreadPoolExecutorс

ScheduledThreadPoolExecutor имеет методы, аналогичные классуTimer:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
ScheduledFuture resultFuture
  = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);


Чтобы завершить наш пример, мы используемscheduleAtFixedRate() для повторяющихся задач:

ScheduledFuture resultFuture
 = executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);


Приведенный выше код выполнит задачу после начальной задержки в 100 миллисекунд, а после этого она будет выполнять ту же задачу каждые 450 миллисекунд.

Если процессор не может завершить обработку задачи вовремя до следующего возникновения,ScheduledExecutorService будет ждать завершения текущей задачи, прежде чем начинать следующую.

Чтобы избежать этого времени ожидания, мы можем использоватьscheduleWithFixedDelay(), который, как описано в его названии, гарантирует фиксированную задержку длины между итерациями задачи.

Для получения дополнительной информации оScheduledExecutorService, прочтите нашGuide to the Java ExecutorService.

6.3. Какой инструмент лучше?

Если мы запустим приведенные выше примеры, результат вычислений будет таким же.

Итак,how do we choose the right tool?

Когда фреймворк предлагает несколько вариантов, важно понимать базовую технологию, чтобы принять обоснованное решение.

Давайте попробуем погрузиться немного глубже под капот.

Timer:

  • не предлагает гарантий в реальном времени: он планирует задачи с помощью методаObject.wait(long) 

  • есть один фоновый поток, поэтому задачи выполняются последовательно, а длительная задача может задерживать выполнение других

  • исключения времени выполнения, брошенные вTimerTask, уничтожат единственный доступный поток, тем самым убивTimer

ScheduledThreadPoolExecutor:

  • можно настроить с любым количеством потоков

  • может использовать все доступные ядра процессора

  • перехватывает исключения времени выполнения и позволяет нам обрабатывать их, если мы хотим (переопределив методafterExecute изThreadPoolExecutor)

  • отменяет задачу, которая вызвала исключение, позволяя другим продолжать работать

  • опирается на систему планирования ОС для отслеживания часовых поясов, задержек, солнечного времени и т. д.

  • предоставляет совместный API, если нам нужна координация между несколькими задачами, например, ожидание завершения всех представленных задач

  • обеспечивает лучший API для управления жизненным циклом потока

Выбор сейчас очевиден, верно?

7. Разница междуFuture иScheduledFuture

В наших примерах кода мы можем заметить, чтоScheduledThreadPoolExecutor возвращает определенный типFuture:ScheduledFuture.

ScheduledFuture расширяет интерфейсы какFuture, так иDelayed, таким образом наследуя дополнительный методgetDelay, который возвращает оставшуюся задержку, связанную с текущей задачей. Он расширенRunnableScheduledFuture, который добавляет метод проверки периодичности задачи.

ScheduledThreadPoolExecutor реализует все эти конструкции через внутренний классScheduledFutureTask и использует их для управления жизненным циклом задачи.

8. Выводы

В этом уроке мы экспериментировали с различными структурами, доступными для запуска потоков и параллельного выполнения задач.

Затем мы углубились в различия междуTimer иScheduledThreadPoolExecutor.

Исходный код статьи доступенover on GitHub.