JavaRush /Курсы /JAVA 25 SELF /Запуск потоков: Thread и Runnable, синтаксис

Запуск потоков: Thread и Runnable, синтаксис

JAVA 25 SELF
51 уровень , 1 лекция
Открыта

1. Класс Thread: первый поток в Java

В Java каждый поток исполнения представлен объектом класса Thread. Этот класс — как дирижёр в оркестре: именно он управляет запуском, остановкой и жизненным циклом потока.

Чтобы запустить поток, нужно:

  1. Создать объект класса Thread (или его наследника).
  2. Вызвать у этого объекта метод start().

Давайте разберёмся на практике.

Пример 1. Наследование от Thread

Самый прямолинейный способ создать поток — унаследовать свой класс от Thread и переопределить его метод run(). Всё, что вы напишете внутри run(), выполнится в отдельном потоке.

// Простой поток через наследование
public class HelloThread extends Thread {
    @Override
    public void run() {
        System.out.println("Привет из потока! Меня зовут: " + getName());
    }
}

public class Main {
    public static void main(String[] args) {
        HelloThread thread = new HelloThread(); // создаём объект-поток
        thread.start(); // запускаем поток
        System.out.println("Главный поток завершает работу.");
    }
}

Что происходит на самом деле?
Когда вы вызываете thread.start(), Java создаёт новый поток и уже внутри него запускает метод run(). При этом основной поток (тот самый, который начал с main) не ждёт и продолжает свою работу параллельно.

И главное предупреждение: не путайте start() с прямым вызовом run(). Если написать thread.run(), никакого нового потока не будет — это просто обычный метод, который выполнится в том же самом потоке, где вы его вызвали. Настоящее многопоточие начинается только с start().

Что такое getName()?

Метод getName() возвращает имя потока. По умолчанию Java даёт потокам имена вида "Thread-0", "Thread-1" и т.д. Это удобно для отладки.

2. Интерфейс Runnable: лучший способ для большинства случаев

Java — язык, который любит гибкость. Класс Thread уже наследует от Object, а в Java нет множественного наследования классов. Если вы унаследуете свой класс от Thread, то не сможете унаследовать его ещё от чего-то (например, если у вас есть свой класс Car, и вы захотите сделать его потоком — придётся выбирать).

Решение: выносить логику потока в отдельный объект, реализующий интерфейс Runnable. Это интерфейс с одним методом run(). Затем вы создаёте объект класса Thread, передаёте ему свой Runnable — и запускаете поток.

Пример 2. Класс с Runnable

// Класс, реализующий Runnable
public class HelloRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Привет из Runnable! Поток: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        Runnable runnable = new HelloRunnable();
        Thread thread = new Thread(runnable); // передаём Runnable в Thread
        thread.start();
        System.out.println("Главный поток завершает работу.");
    }
}

Здесь мы создаём класс HelloRunnable, который реализует интерфейс Runnable. В методе main создаётся объект этого класса, и он передаётся в конструктор Thread. После этого поток запускается с помощью вызова start().

Отдельно стоит упомянуть метод Thread.currentThread(). Это статический метод, который позволяет получить объект текущего потока и тем самым понять, где именно выполняется наш код.

Почему это круче?

  • Ваш класс может наследоваться от любого другого класса (а не только от Thread).
  • Можно переиспользовать один и тот же Runnable для запуска в нескольких потоках.
  • Код становится более гибким и чистым.

3. Синтаксис: анонимные классы и лямбда-выражения

Java не стоит на месте! После Java 8 мы можем писать ещё короче и удобнее.

Пример 3. Анонимный класс

Иногда не хочется создавать отдельный файл для класса, который нужен один раз. Можно объявить его прямо в месте использования — это называется анонимный класс.

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Анонимный Runnable! Поток: " + Thread.currentThread().getName());
            }
        });
        thread.start();
        System.out.println("Главный поток завершает работу.");
    }
}

Пример 4. Лямбда-выражение (Java 8+)

Интерфейс Runnable — функциональный (всего один абстрактный метод). Поэтому можно использовать лямбда-выражение:

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Лямбда-поток! Поток: " + Thread.currentThread().getName());
        });
        thread.start();
        System.out.println("Главный поток завершает работу.");
    }
}

Кратко:
Runnable реализуется «на месте», а лямбда-выражение предоставляет реализацию метода run().

4. Запуск нескольких потоков

Запускать можно сколько угодно потоков (ну, почти — всё зависит от ресурсов компьютера).

Пример 5. Запускаем несколько потоков

public class Main {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            int threadNumber = i; // обязательно делать копию переменной для лямбды!
            Thread thread = new Thread(() -> {
                System.out.println("Поток #" + threadNumber + ": привет!");
            });
            thread.start();
        }
        System.out.println("Главный поток завершает работу.");
    }
}

Вывод может быть разным!
Потоки работают параллельно — кто первым выведет своё сообщение, зависит от ОС и планировщика потоков.

5. Жизненный цикл потока

Понимание того, как живёт поток, важно для отладки и правильного использования потоков.

Основные состояния потока

  • NEW — поток создан, но ещё не запущен (new Thread(...)).
  • RUNNABLE — поток запущен (start()), может выполняться.
  • TERMINATED — поток завершил выполнение (метод run() закончился).

Что происходит при вызове start() и run()

  • start() — создаёт новый поток и вызывает его метод run() в этом новом потоке.
  • run() — обычный метод; если вызвать напрямую, он выполнится в текущем потоке (не создаст новый поток!).

Пример:

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("run() из потока: " + Thread.currentThread().getName()));
        thread.run();   // ВЫЗЫВАЕТСЯ В ГЛАВНОМ ПОТОКЕ!
        thread.start(); // ВЫЗЫВАЕТСЯ В ОТДЕЛЬНОМ ПОТОКЕ!
    }
}

6. Практика: делаем простое многопоточное приложение

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

public class LoggerTask implements Runnable {
    private final String message;

    public LoggerTask(String message) {
        this.message = message;
    }

    @Override
    public void run() {
        System.out.println("[" + Thread.currentThread().getName() + "] Лог: " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Thread logger1 = new Thread(new LoggerTask("Запуск системы"));
        Thread logger2 = new Thread(new LoggerTask("Загрузка данных"));
        Thread logger3 = new Thread(new LoggerTask("Обработка запроса"));

        logger1.start();
        logger2.start();
        logger3.start();

        System.out.println("Главный поток завершает работу.");
    }
}

Что происходит?
Три потока параллельно пишут свои сообщения в консоль. Порядок сообщений не гарантируется — это и есть суть многопоточности.

7. Полезные нюансы

Таблица: сравнение способов создания потоков

Способ Гибкость Переиспользование Рекомендуется?
Наследование от Thread Низкая Нет Только для простых/учебных примеров
Реализация Runnable Высокая Да Да
Анонимный класс Средняя Нет Для одноразовых задач
Лямбда-выражение Высокая Нет Да (Java 8+)

Схема: как работает запуск потока

+----------------------+ 
|  main (главный поток)|
+----------------------+
           |
           v
+----------------------+
|  Thread thread = ... |
+----------------------+
           |
           v
+----------------------+
|  thread.start()      |  ← Поток "оживает"
+----------------------+
           |
           v
+----------------------+
|  run() в новом потоке|
+----------------------+
           |
           v
+----------------------+
|  Поток завершён      |
+----------------------+

8. Типичные ошибки при запуске потоков

Ошибка №1: Вызов run() вместо start().
Очень частая ошибка новичков — вызвать у объекта потока метод run() напрямую. В этом случае никакого нового потока не создаётся, метод просто выполняется в текущем (главном) потоке. Правильно: всегда использовать start() для запуска потока.

Ошибка №2: Повторный запуск потока.
Нельзя вызвать start() у одного и того же объекта Thread более одного раза — это вызовет IllegalThreadStateException. Если вам нужно снова запустить задачу, создайте новый объект Thread.

Ошибка №3: Изменение общих переменных без синхронизации.
Если несколько потоков обращаются к одним и тем же переменным, результат может быть неожиданным (race condition). Про синхронизацию поговорим позже, но пока — будьте осторожны.

Ошибка №4: Не учли завершение потоков.
Если важно дождаться завершения потока, используйте метод join(). Иначе основной поток может завершиться раньше, чем дочерние.

Ошибка №5: Путают имя потока и имя класса.
Метод getName() возвращает имя потока, а не имя класса. Для отладки всегда полезно явно задавать имена потокам через конструктор или setName().

1
Задача
JAVA 25 SELF, 51 уровень, 1 лекция
Недоступна
Запуск беспилотного дрона-курьера ✈️
Запуск беспилотного дрона-курьера ✈️
1
Задача
JAVA 25 SELF, 51 уровень, 1 лекция
Недоступна
Поручение задачи новому стажёру 🧑‍💻
Поручение задачи новому стажёру 🧑‍💻
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ