Проблеми, які вирішує багатопоточність у Java
По суті, багатопоточність Java була придумана, щоб вирішити дві основні завдання:-
Одночасно виконувати кілька дій.
У прикладі вище різні потоки (тобто члени сім'ї) паралельно виконували кілька дій: мабо посуд, ходабо в магазин, складали речі.
Можна навести і більш «програмістський» приклад. Уяви, що в тебе є програма з інтерфейсом користувача. При натисканні кнопки "Продовжити" всередині програми повинні відбутися якісь обчислення, а користувач має побачити наступний екран інтерфейсу. Якщо ці дії здійснюються послідовно, після натискання кнопки "Продовжити" програма просто зависне. Користувач буде бачити той самий екран з кнопкою «Продовжити», поки всі внутрішні обчислення не будуть виконані, і програма не дійде до частини, де почнеться малювання інтерфейсу.
Що ж, зачекаємо кілька хвабон!
А ще ми можемо переробити нашу програму, або, як кажуть програмісти, розпаралелити. Нехай необхідні обчислення виконуються в одному потоці, а малювання інтерфейсу - в іншому. Більшість комп'ютерів вистачить цього ресурсів. У такому разі програма не буде «тупити», і користувач буде спокійно переходити між екранами інтерфейсу, не переймаючись тим, що відбувається всередині. Одне іншому не заважає :)
-
Прискорити обчислення.
Тут все набагато простіше. Якщо наш процесор має кілька ядер, а більшість процесорів зараз є багатоядерними, список наших завдань можуть паралельно вирішувати кілька ядер. Очевидно, що якщо нам потрібно вирішити 1000 завдань і кожна з них вирішується за секунду, одне ядро впорається зі списком за 1000 секунд, два ядра — за 500 секунд, три — за 333 секунди і так далі.
Thread
. Тобто щоб створити та запустити виконання 10 потоків, знадобиться 10 об'єктів цього класу. Напишемо найпростіший приклад:
public class MyFirstThread extends Thread {
@Override
public void run() {
System.out.println("I'm Thread! My name is " + 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();
}
}
}
Висновок у консоль: I'm Thread! My name is Thread-2 I'm Thread! My name is Thread-1 I'm Thread! My name is Thread-0 I'm Thread! My name is Thread-3 I'm Thread! My name is Thread-6 I'm Thread! My name is Thread-7 I'm Thread! My name is Thread-4 I'm Thread! My name is Thread-5 I'm Thread! My name is Thread-9 I'm Thread! My name is Thread-8 Створюємо 10 потоків (об'єктів) MyFirstThread
, який успадковується Thread
і запускаємо їх, викликаючи у об'єкта метод start()
. Після виклику методу start()
починає роботу його метод run()
і виконується та логіка, яка була в ньому написана. Зверніть увагу: імена потоків йдуть не по порядку. Це досить дивно, чому вони не виконувались по черзі Thread-0
:Thread-1
Thread-2
і так далі? Це приклад того, коли стандартне, «послідовне» мислення не підійде. Справа в тому, що ми в даному випадку лише віддаємо команди на створення та запуск 10 потоків. В якому порядку їх запускати – вирішує планувальник потоків: особливий механізм усередині операційної системи. Як саме він влаштований і за яким принципом приймає рішення — тема дуже складна, і зараз не занурюватимемося в неї. Головне запам'ятай, що послідовність виконання потоків програміст контролювати не може. Щоб усвідомити серйозність ситуації, спробуй запустити метод main()
з прикладу ще кілька разів. Другий висновок у консоль: I'm Thread! My name is Thread-0 I'm Thread! My name is Thread-4 I'm Thread! My name is Thread-3 I'm Thread! My name is Thread-2 I'm Thread! My name is Thread-1 I'm Thread! My name is Thread-5 I'm Thread! My name is Thread-6 I'm Thread! My name is Thread-8 I'm Thread! My name is Thread-9 I'm Thread! My name is 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 Веселий вийшов сценарій? :) А все тому, що робота нашої програми залежить від порядку виконання потоків. При найменшому порушенні послідовності наша кухня перетворюється на пекло, а божевільний робот трощить все навколо себе. Це також поширена проблема в багатопотоковому програмуванні, про яку ти ще не раз почуєш. На завершення лекції хочу порадити тобі книгу, присвячену багатопоточності.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ