Зарегистрируйтесь, чтобы продолжить обучение

Многопоточность в Java Java: Продвинутое использование

Потоки в Java

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

Когда программа запускается, JVM создает главный поток, который отвечает за выполнение кода, находящегося в методе main(). Однако, в современных приложениях часто возникает необходимость выполнять несколько задач одновременно, что приводит к необходимости создания дополнительных потоков. Эти дополнительные потоки называются "побочными потоками". Побочные потоки могут быть созданы в заданных программистом местах кода, что позволяет выполнять параллельные задачи. Например, если ваша программа должна обрабатывать данные, загружать файлы или выполнять сетевые запросы, вы можете создать побочные потоки для выполнения этих задач, не блокируя главный поток.

Создание потоков в Java

В Java существует несколько способов создания потоков, каждый из которых имеет свои особенности и подходит для различных сценариев. Рассмотрим основные методы создания потоков

Реализация интерфейса Runnable

Один из способов создания потока — это реализация интерфейса Runnable. В этом случае вы создаете объект класса, реализующего Runnable, и передаете его в конструктор класса Thread. Это позволяет вам использовать один и тот же объект Runnable в нескольких потоках, что может быть полезно для выполнения одной и той же задачи параллельно.

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int k = 0; k < 5; k++) {
                System.out.println("I'm not nain thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("I'm main thread");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Результат выполнения может быть следующий:

I'm main thread
I'm not nain thread
I'm main thread
I'm not nain thread
I'm main thread
I'm not nain thread
I'm main thread
I'm not nain thread
I'm main thread
I'm not nain thread

Порядок вывода сообщений на экран не гарантирован. Когда мы запустили новый поток с помощью метода start(), этот поток начинает выполняться параллельно с основным потоком. Оба потока работают независимо друг от друга, между ними нет никакой синхронизации. В результате, вы можете увидеть различные комбинации вывода

Наследование класса Thread

Еще один способ — это наследование класса Thread. Для этого необходимо создать новый класс, который будет наследовать Thread, и переопределить его метод run(). В этом методе будет описана логика, выполняемая в потоке

public class MyThread extends Thread {

    // В классе нужно переопределить метод run()
    // В методе содержится логика, которую поток будет выполнять
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("My name is MyThread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

При вызове метода start() создается новый поток, и выполнение кода переходит в метод run()

public class Main {

    public static void main(String[] args) {

        Thread myThread = new MyThread();
        myThread.start();

        // Создадим 5 потоков в цикле
        for (int u = 0; u < 5; u++) {
            System.out.println("It is a main thread");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Потоки в программе

В Java программа завершает свою работу, когда все её потоки завершили выполнение. Однако это утверждение не относится к потокам-демонам. Потоки-демоны — это специальные потоки, которые работают в фоновом режиме и предназначены для выполнения вспомогательных задач, таких как сборка мусора или обработка событий. Они отличаются от обычных потоков тем, что их существование не препятствует завершению программы. Когда все обычные потоки завершили свою работу, JVM завершает выполнение программы, даже если потоки-демоны все еще работают.

Завершение потоков

Завершение потоков в Java — это важная тема, требующая внимательного подхода

Завершение потоков в Java — это важная тема, требующая внимательного подхода, поскольку неправильное управление потоками может привести к серьезным проблемам в работе программы. Методы Thread.stop(), Thread.suspend() и Thread.resume() считаются устаревшими и не рекомендуются к использованию. Эти методы могут вызвать непредсказуемые последствия.

Использование метода Thread.stop() завершает поток в любой момент, не давая ему возможности освободить ресурсы или завершить текущие операции. Это может привести к повреждению данных или оставлению ресурсов, таких как файловые дескрипторы, в неосвобожденном состоянии.

Использование метода Thread.suspend() может привести к блокировке потока, который не будет продолжать выполнение, пока не будет вызван Thread.resume(). Это может вызвать зависание всей программы.

Механизм прерываний

Вместо устаревших методов, Java предлагает механизм прерываний, который позволяет потокам получать сигнал о необходимости завершения своей работы. Это делается с помощью метода interrupt(), который устанавливает флаг прерывания для потока. Поток, получивший сигнал о прерывании, может проверить этот флаг и решить, когда и как завершить свою работу.

public class Main {

    public static void main(String[] args) {

        Thread myThread = new MyThread();
        myThread.start();

        // Создадим 5 потоков в цикле
        for (int u = 0; u < 5; u++) {
            System.out.println("It is a main thread");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Thread.currentThread().interrupt();
        }
    }
}

Каждый поток в Java, как наследник класса Thread, содержит метод isInterrupted(), который позволяет проверить, был ли поток прерван. Этот метод возвращает true, если поток был прерван, и false в противном случае. Это позволяет потокам самостоятельно решать, когда и как завершить свою работу, основываясь на состоянии прерывания.

public class MyThread extends Thread {
    @Override
    public void run() {

        if (!Thread.currentThread().isInterrupted()) {
            for (int i = 0; i < 5; i++) {
                System.out.println("My name is MyThread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("I have been interrupted");
                }
            }
        }
    }
}

Ожидание завершения с помощью join

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

Когда один поток вызывает метод join() на другом потоке, вызывающий поток приостанавливает своё выполнение до тех пор, пока целевой поток не завершит свою работу. Это особенно полезно в ситуациях, когда необходимо гарантировать, что определённые задачи будут выполнены до того, как продолжится выполнение основного потока. Например, если у вас есть поток, который выполняет важные вычисления или обрабатывает данные, и вы хотите дождаться его завершения, прежде чем переходить к следующему этапу программы/

Метод join() также имеет перегруженную версию, которая принимает в качестве параметра время ожидания в миллисекундах. Это позволяет потоку ожидать завершения другого потока только в течение определённого времени. Если целевой поток завершится до истечения этого времени, вызывающий поток продолжит выполнение. Если же время ожидания истечёт, вызывающий поток продолжит выполнение, даже если целевой поток всё ещё работает. Это может быть полезно в ситуациях, когда необходимо избежать бесконечного ожидания, например, если вы хотите, чтобы программа продолжала работать, даже если один из потоков завис или выполняется слишком долго.

class JoinThread extends Thread {
    @Override
    public void run() {
        System.out.println("JoinThread: Начало работы.");
        try {
            // Имитация длительной работы потока
            Thread.sleep(3000); // Поток работает 3 секунды
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("JoinThread: Завершение работы.");
    }
}
public class Example1 {

    public static void main(String[] args) {

        // Создается новый поток JoinThread
        Thread thread = new JoinThread();

        // Запускается поток
        thread.start();

        System.out.println("Основной поток: Breakpoint 1");

        // Основной поток ожидает завершения потока thread
        // Выполнение основного потока приостанавливается до тех пор, пока thread не выполнится полностью
        try {
            System.out.println("Основной поток: Ожидание завершения потока JoinThread...");
            thread.join(); // Ожидание завершения потока
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Основной поток: Breakpoint 2");
    }
}
Основной поток: Breakpoint 1
JoinThread: Начало работы.
Основной поток: Ожидание завершения потока JoinThread...
JoinThread: Завершение работы.
Основной поток: Breakpoint 2

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

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

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff