1. Клас Thread: перший потік у Java
У Java кожен потік виконання представлений об’єктом класу Thread. Цей клас — як диригент в оркестрі: саме він керує запуском, зупинкою та життєвим циклом потоку.
Щоб запустити потік, потрібно:
- Створити об’єкт класу Thread (або його нащадка).
- Викликати в цього об’єкта метод 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().
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ