Привіт! Сьогодні продовжуємо говорити про багатопоточність. Розглянемо клас Thread і роботу його кількох методів. Раніше, коли ми вивчали методи класу, найчастіше просто писали це так: «назва методу» -> «що він робить».
З методами Thread так не вийде :) Їх логіка складніша, і без кількох прикладів не розібратись.
Давай пригадаємо приклад з попередньої лекції:
Метод
Метод

Метод Thread.start()
Почнемо з повторення. Як ти напевно пам'ятаєш, створити потік можна успадкувавши свій клас від класуThread і перевизначивши в ньому метод run().
Але сам він, звісно, не запуститься. Для цього у нашого об'єкта викликаємо метод start().
Давай пригадаємо приклад з попередньої лекції:
public class MyFirstThread extends Thread {
@Override
public void run() {
System.out.println("Виконано потік " + getName());
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyFirstThread thread = new MyFirstThread();
thread.start();
}
}
}
Зверни увагу: щоб запустити потік, потрібно викликати спеціальний метод start(), а не метод run()! Цю помилку легко допустити, особливо на початку вивчення багатопоточності.
Якщо у нашому прикладі ти 10 разів викличеш у об'єкта метод run() замість start(), результат буде таким:
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyFirstThread thread = new MyFirstThread();
thread.run();
}
}
}
Виконано потік Thread-0
Виконано потік Thread-1
Виконано потік Thread-2
Виконано потік Thread-3
Виконано потік Thread-4
Виконано потік Thread-5
Виконано потік Thread-6
Виконано потік Thread-7
Виконано потік Thread-8
Виконано потік Thread-9
Подивись на послідовність виводу: все йде строго по порядку. Дивно, правда? Ми до такого не звикли, адже вже знаємо, що порядок запуску і виконання потоків визначає "суперрозум" в операційній системі — планувальник потоків. Можливо, просто пощастило?
Звісно, справа не у везінні. У цьому ти можеш переконатись, запустивши програму ще пару разів.
Справа у тому, що прямий виклик методу run() не має відношення до багатопоточності. У цьому випадку програма виконається у головному потоці — тому самому, в якому виконується метод main(). Вона просто послідовно виведе 10 рядків у консоль і все. Ніякі 10 потоків не запустяться.
Тому запам'ятай на майбутнє і постійно себе перевіряй. Хочеш, щоб виконався run(), викликай start(). Поїхали далі.
Метод Thread.sleep()
Для призупинення виконання поточного потоку на певний час, використовуємо методsleep().
Метод sleep() приймає як параметр число мілісекунд, тобто той час, на який потрібно «приспати» потік.
public class Main {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
Thread.sleep(3000);
System.out.println(" - Скільки я проспав? \n - " + ((System.currentTimeMillis()-start)) / 1000 + " секунди");
}
}
Вивід у консоль:
- Скільки я проспав?
- 3 секунди
Зверни увагу: метод sleep() — статичний: він присипляє поточний потік. Тобто той, що працює у даний момент.
Ще один важливий нюанс: потік у стані сну можна перервати. У такому разі у програмі виникне виключення InterruptedException. Ми розглянемо приклад нижче.
До речі, а що станеться після того, як потік «прокинеться»? Чи продовжить він відразу своє виконання з того місця, де закінчив? Ні.
Після того, як потік «прокидається» — коли закінчується час, переданий як аргумент у Thread.sleep(), — він переходить у стан runnable, «готовий до роботи». Однак це не означає, що планувальник потоків запустить саме його. Цілком можливо, він віддасть перевагу якомусь іншому «не сплячому» потоку, а наш «щойно прокинувшийся» продовжить роботу трохи пізніше.
Обов'язково запам'ятай: «прокинувся — не означає продовжив роботу у ту ж секунду»!
Метод Thread.join()
Метод join() призупиняє виконання поточного потоку до тих пір, поки не завершиться інший потік.
Якщо у нас є 2 потоки, t1 і t2, і ми напишемо —
t1.join()
t2 не почне працювати, доки t1 не завершить свою. Метод join() можна використовувати, щоб гарантувати послідовність виконання потоків.
Давай розглянемо роботу join() на прикладі:
public class ThreadExample extends Thread {
@Override
public void run() {
System.out.println("Початок роботи потоку " + getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Потік " + getName() + " завершив роботу.");
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
ThreadExample t1 = new ThreadExample();
ThreadExample t2 = new ThreadExample();
t1.start();
/*Другий потік t2 почне виконання лише після того, як буде завершено
(або кинуте виключення) перший потік - t1*/
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
//Головний потік продовжить роботу лише після того, як t1 і t2 завершать роботу
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Усі потоки завершили роботу, програма завершена");
}
}
Ми створили простий клас ThreadExample. Його задача — вивести на екран повідомлення про початок роботи, потім заснути на 5 секунд і в кінці повідомити про завершення роботи. Нічого складного.
Головна логіка зосереджена у класі Main. Подивись на коментарі: за допомогою методу join() ми успішно керуємо послідовністю виконання потоків. Якщо ти згадаєш початок теми, цим займався планувальник потоків. Він запускав їх на свій розсуд: кожного разу по-різному.
Тут же ми за допомогою методу гарантували, що спочатку буде запущений і виконаний потік t1, потім — t2, і лише після них — головний потік виконання програми.
Їдемо далі.
У реальних програмах тобі не раз зустрінуться ситуації, коли потрібно буде перервати виконання якогось потоку.
Наприклад, наш потік виконується, але при цьому чекає певної події або виконання умови. Якщо це сталося, він зупиняється. Було б, мабуть, логічно, якби існував якийсь метод типу stop().
Проте всі не так просто. Колись давно метод Thread.stop() у Java дійсно був і дозволяв переривати роботу потоку. Але пізніше його видалили з бібліотеки Java. Ти можеш знайти його у документації Oracle і побачити, що він позначений як deprecated..
Чому? Тому що він просто зупиняв потік без будь-якої додаткової роботи.
Наприклад, потік міг працювати з даними й щось у них змінювати. Потім його різко вимикали методом stop() посеред роботи — і все. Ні коректного завершення роботи, ні звільнення ресурсів, ні хоча б обробки помилок — нічого цього не було.
Метод stop(), якщо перебільшити, просто руйнував усе на своєму шляху.
Його роботу можна порівняти з тим, як хтось висмикує вилку з розетки, щоб вимкнути комп'ютер. Так, необхідного результату досягти можна. Але всі розуміють, що через кілька тижнів комп'ютер не скаже за це «дякую».
З цієї причини логіку переривання потоків у Java змінили, і тепер використовується спеціальний метод — interrupt().
Метод Thread.interrupt()
Що станеться, якщо у потоку викликати методinterrupt()?
Є 2 варіанти:
- Якщо об'єкт знаходився в цей момент у стані очікування, наприклад,
joinабоsleep, очікування буде перервано, і програма викинеInterruptedException. - Якщо ж потік у цей момент був у працездатному стані, у об'єкта буде встановлено boolean-флаг
interrupted.
Thread є спеціальний метод — boolean isInterrupted().
Давай повернемося до прикладу з годинником, який був у лекції основного курсу. Для зручності він трохи спрощений:
public class Clock extends Thread {
public static void main(String[] args) throws InterruptedException {
Clock clock = new Clock();
clock.start();
Thread.sleep(10000);
clock.interrupt();
}
public void run() {
Thread current = Thread.currentThread();
while (!current.isInterrupted())
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Робота потоку була перервана");
break;
}
System.out.println("Tik");
}
}
}
У нашому випадку годинник починає працювати і починає тикати кожну секунду. На 10-й секунді ми перериваємо потік годинника.
Як ти вже знаєш, якщо потік, який ми намагаємося перервати, знаходиться в одному зі станів очікування, це призводить до InterruptedException. Цей вид винятку — перевіряємий, тому його можна легко перехопити і виконати нашу логіку завершення програми. Що ми і зробили.
Ось наш результат:
Tik
Tik
Tik
Tik
Tik
Tik
Tik
Tik
Tik
Робота потоку була перервана
На цьому ми завершуємо знайомство з основними методами класу Thread.
Успіхів!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ