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().

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ