Уяви, що твоя сім’я переїжджає з одного будинку в інший. Важлива частина переїзду — зібрати книги. Книг у вас накопичилось багато, і треба скласти їх у коробки.
Зараз вільний тільки ти. Мама готує їжу, брат збирає одяг, а сестра пішла в магазин. Самотужки ти якось впораєшся, і, рано чи пізно, навіть сам виконаєш завдання, але часу знадобиться дуже багато. Втім, за 20 хвилин із магазину повернеться твоя сестра, і інших справ у неї немає. Тож вона може приєднатись до тебе.
Завдання не змінилося: скласти книги в коробки. Тільки виконується вдвічі швидше. Чому?
Бо робота виконується паралельно. Два різних «потоки» (ти і твоя сестра) одночасно виконують одне й те саме завдання і, якщо нічого не зміниться, різниця в часі буде дуже значною порівняно з ситуацією, коли б ти робив усе сам.
Якщо брат скоро впорається зі своєю задачою, він може допомогти вам, і справа піде ще швидше.
Проблеми, які вирішує багатопоточність у Java
По суті, багатопоточність Java була придумана, щоб вирішити дві головні задачі:Одночасно виконувати кілька дій.
У прикладі вище різні потоки (тобто члени сім’ї) паралельно виконували кілька дій: мили посуд, ходили в магазин, складали речі.
Можна навести й більш «програмістський» приклад. Уяви, що у тебе є програма з користувацьким інтерфейсом. При натисканні кнопки «Продовжити» всередині програми мають відбутися якісь обчислення, а користувач повинен побачити наступний екран інтерфейсу. Якщо ці дії здійснюються послідовно, після натискання кнопки «Продовжити» програма просто зависне. Користувач бачитиме той самий екран із кнопкою «Продовжити», поки всі внутрішні обчислення не будуть виконані, й програма не дійде до частини, де почнеться відтворення інтерфейсу.
Що ж, почекаємо пару хвилин!
![Багатопоточність у Java: суть, «плюси» і часті пастки - 3]()
А ще ми можемо переробити нашу програму, або, як кажуть програмісти, «розпаралелити». Нехай потрібні обчислення виконуються в одному потоці, а відтворення інтерфейсу — в іншому. У більшості комп’ютерів вистачить на це ресурсів. У такому випадку програма не буде «гальмувати», і користувач буде спокійно переходити між екранами інтерфейсу, не турбуючись про те, що відбувається всередині. Одне одному не заважає :)
Прискорити обчислення.
Тут усе набагато простіше. Якщо наш процесор має кілька ядер, а більшість процесорів зараз багатоядерні, список наших задач можуть паралельно вирішувати кілька ядер. Очевидно, що якщо нам потрібно вирішити 1000 задач і кожна з них вирішується за секунду, одне ядро впорається зі списком за 1000 секунд, два ядра — за 500 секунд, три — за 333 з невеликим секунди і так далі.
Thread. Тобто, щоб створити й запустити виконання 10 потоків, знадобиться 10 об'єктів цього класу.
Напишемо найпростіший приклад:
public class MyFirstThread extends Thread {
@Override
public void run() {
System.out.println("Я потік! Моє ім'я " + getName());
}
}
Щоб формувати й запускати потоки, нам потрібно створити клас, успадкувати його від класу java.lang.Thread і перевизначити в ньому метод run().
Останнє — дуже важливо. Саме в методі run() ми прописуємо ту логіку, яку наш потік має виконати.
Тепер, якщо ми створимо екземпляр MyFirstThread і запустимо його, метод run() виведе в консоль рядок із його ім’ям: метод getName() виводить «системне» ім’я потоку, яке призначається автоматично.
Хоча, власне, чому «якщо»? Давай створимо та перевіримо!
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyFirstThread thread = new MyFirstThread();
thread.start();
}
}
}
Вивід у консоль:
Я потік! Моє ім'я Thread-2
Я потік! Моє ім'я Thread-1
Я потік! Моє ім'я Thread-0
Я потік! Моє ім'я Thread-3
Я потік! Моє ім'я Thread-6
Я потік! Моє ім'я Thread-7
Я потік! Моє ім'я Thread-4
Я потік! Моє ім'я Thread-5
Я потік! Моє ім'я Thread-9
Я потік! Моє ім'я Thread-8
Створюємо 10 потоків (об'єктів) MyFirstThread, що успадковується від Thread, і запускаємо їх, викликаючи у об’єкта метод start(). Після виклику методу start() починає роботу його метод run(), і виконується та логіка, яка була в ньому написана.
Зверни увагу: імена потоків ідуть не за порядком. Це доволі дивно, чому вони не виконувалися по черзі: Thread-0, Thread-1, Thread-2 і так далі?
Це якраз приклад того, коли стандартне, «послідовне» мислення не підійде. Справа в тому, що ми в даному випадку тільки віддаємо команди на створення і запуск 10 потоків. У якому порядку їх запускати — вирішує планувальник потоків: особливий механізм у межах операційної системи.
Як саме він влаштований і за яким принципом приймає рішення — тема дуже складна, і зараз не будемо в неї заглиблюватися. Головне запам’ятай, що послідовність виконання потоків програміст контролювати не може.
Щоб усвідомити серйозність ситуації, спробуй запустити метод main() із прикладу вище ще пару разів.
Другий вивід у консоль:
Я потік! Моє ім'я Thread-0
Я потік! Моє ім'я Thread-4
Я потік! Моє ім'я Thread-3
Я потік! Моє ім'я Thread-2
Я потік! Моє ім'я Thread-1
Я потік! Моє ім'я Thread-5
Я потік! Моє ім'я Thread-6
Я потік! Моє ім'я Thread-8
Я потік! Моє ім'я Thread-9
Я потік! Моє ім'я Thread-7
Третій вивід в консоль:
I'm Thread! My name is Thread-0
I'm Thread! My name is Thread-3
I'm Thread! My name is Thread-1
I'm Thread! My name is Thread-2
I'm Thread! My name is Thread-6
I'm Thread! My name is Thread-4
I'm Thread! My name is Thread-9
I'm Thread! My name is Thread-5
I'm Thread! My name is Thread-7
I'm Thread! My name is Thread-8
Проблеми, які створює багатопоточність
На прикладі з книгами ти побачив, що багатопоточність вирішує досить важливі задачі, і її використання пришвидшує роботу наших програм. У багатьох випадках — у рази. Але багатопоточність недарма вважається складною темою. Адже при неправильному використанні вона створює проблеми замість того, щоб їх вирішувати. Говорячи «створювати проблеми» я не маю на увазі щось абстрактне. Є дві конкретні проблеми, які може викликати використання багатопоточності — взаємне блокування (deadlock) і стан гонки (race condition). Deadlock — ситуація, при якій кілька потоків знаходяться в стані очікування ресурсів, зайнятих один одним, і жоден з них не може продовжувати виконання. Ми ще поговоримо про нього у наступних лекціях, поки достатньо цього прикладу:
Уяви, що потік-1 працює з якимось Об'єктом-1, а потік-2 працює з Об'єктом-2. При цьому програма написана так:
- Потік-1 припинить працювати з Об'єктом-1 і переключиться на Об'єкт-2, як тільки Потік-2 припинить працювати з Об'єктом 2 і переключиться на Об'єкт-1.
- Потік-2 припинить працювати з Об'єктом-2 і переключиться на Об'єкт-1, як тільки Потік-1 припинить працювати з Об'єктом 1 і переключиться на Об'єкт-2.
Стан гонки — помилка проєктування багатопоточної системи або програми, при якій робота системи або програми залежить від того, в якому порядку виконуються частини коду.
Згадай приклад із запуском потоків:
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();
}
}
}
А тепер уяви, що програма відповідає за роботу робота, який готує їжу!
Потік-0 дістає яйця з холодильника.
Потік-1 вмикає плиту.
Потік-2 дістає сковорідку і ставить на плиту.
Потік-3 запалює вогонь на плиті.
Потік-4 виливає на сковорідку масло.
Потік-5 розбиває яйця і виливає їх на сковорідку.
Потік-6 викидає шкаралупу в сміттєвий кошик.
Потік-7 знімає готову яєчню з вогню.
Потік-8 викладає яєчню в тарілку.
Потік-9 миє посуд.
Поглянь на результати роботи нашої програми:
Виконано потік Thread-0
Виконано потік Thread-2
Виконано потік Thread-1
Виконано потік Thread-4
Виконано потік Thread-9
Виконано потік Thread-5
Виконано потік Thread-8
Виконано потік Thread-7
Виконано потік Thread-3
Виконано потік Thread-6
Веселий вийшов сценарій? :) А все тому, що робота нашої програми залежить від порядку виконання потоків.
При найменшому порушенні послідовності наша кухня перетворюється на хаос, а зійшовший з розуму робот трощить усе навколо себе. Це теж поширена проблема у багатопоточному програмуванні, про яку ти ще не раз почуєш.
На завершення лекції хочу порадити тобі книгу, присвячену багатопоточності.


ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ