Введение в многопоточность Java

Вернуться назад

25 апреля 2019

Есть два способа создания потоков в Java: унаследоваться от класса Thread или реализовать интерфейс Runnable.

Создание потока через наследование

Рассмотрим пример, в котором мы расширяем стандартный класс Thread, переопределяя лишь один метод run(). В нём мы ждём 3 секунды, а затем выводим сообщение о завершении потока. Обратите внимание, что запуск потока осуществляется не через метод run(), а через метод start().

public class ThinkerThread extends Thread {

    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(3); // аналогично Thread.sleep(3000L);
            System.out.println("Второй поток завершён");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        new ThinkerThread().start();
        System.out.println("Основной поток завершён");
    }
}

Метод TimeUnit.SECONDS.sleep() полностью эквивалентен стандартному Thread.sleep(), однако в первом случае мы явно указываем единицы измерения времени, что делает код более читаемым. Я рекомендую использовать именно такую форму записи.

Поскольку метод sleep() относится к низкоуровневым, он может кидать InterruptedException, которое свидетельствует о том, что поток был прерван. Это исключение является единственным признаком того, что поток был принудительно остановлен. И чтобы не терять эту информацию в стеке вызовов, мы выставляем флаг interrupted при помощи соответствующего метода Thread.currentThread(). Такой способ обработки InterruptedException является правильным и именно его нужно использовать в подобных случаях.

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

Создание потока через интерфейс

Теперь создадим поток с помощью интерфейса Runnable.

public class ThinkerRunnable implements Runnable {

    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println("Второй поток завершён");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        new Thread(new ThinkerRunnable()).start();
        System.out.println("Основной поток завершён");
    }
}

Интерфейс Runnable требует определить единственный метод run(). Для запуска потока требуется создать объект Thread, передав ему в конструктор экземпляр нашего класса. Затем у этого объекта Thread вызываем метод start(). Данный вариант является более предпочтительным, если наш класс уже наследуется от какого-то другого базового класса.

Потоки-демоны

В Java процесс завершается тогда, когда завершается последний его поток. Даже если метод main() уже завершился, но еще выполняются порожденные им потоки, система будет ждать их завершения.

Однако это правило не относится к особому виду потоков - демонам. Если завершился последний обычный поток процесса, и остались только потоки-демоны, то они будут принудительно завершены и выполнение процесса закончится. Чаще всего потоки-демоны используются для выполнения фоновых задач, обслуживающих процесс в течение его жизни.

Сделать поток демоном очень просто - достаточно установить соответствующий флаг с помощью метода setDaemon().

public class DaemonThread extends Thread {

    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println("Поток-демон завершён");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        Thread thread = new DaemonThread();
        thread.setDaemon(true);
        thread.start();
        System.out.println("Основной поток завершён");
    }
}

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

Как остановить поток

Остановить поток можно с помощью метода interrupt(). В случае, если поток не является демоном, то возникает исключение InterruptedException, о котором мы говорили выше.

public class ThinkerThread extends Thread {

    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println("Второй поток завершён");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        Thread thread = new ThinkerThread();
        thread.start();
        System.out.println("Основной поток завершён");
        thread.interrupt();
    }
}

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

public class ThinkerThread extends Thread {

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            Math.cos(Integer.MAX_VALUE);
        }
        Thread.currentThread().interrupt();
        System.out.println("Второй поток завершён");
    }

    public static void main(String[] args) {
        Thread thread = new ThinkerThread();
        thread.start();
        System.out.println("Основной поток завершён");
        thread.interrupt();
    }
}

Перед каждой итерацией цикла мы проверяем флаг Thread.currentThread().isInterrupted(). И если он оказывается установлен, мы прерываем наши вычисления. Таким образом, мы корректно обрабатываем флаг остановки потока.

Тэги: Java, многопоточность.



Добавить комментарий

Ваше имя:
Текст комментария: