JavaRush /Java блог /Random UA /Багатопотоковість в Java: суть, «плюси» та часті пастки

Багатопотоковість в Java: суть, «плюси» та часті пастки

Стаття з групи Random UA
Вітання! Насамперед, вітаю: ти дійшов до теми Багатопоточність у Java! Це серйозне досягнення, позаду чималий шлях. Але приготуйся: це одна з найважчих тем у курсі. І річ не в тому, що тут використовуються складні класи чи багато методів: навпаки, і двох десятків не набереться. Швидше в тому, що потрібно трохи змінити мислення. Раніше твої програми виконувалися послідовно. Одні рядки коду йшли після інших, одні методи за іншими, і в цілому все було зрозуміло. Спочатку порахувати щось, потім вивести результат на консоль, потім завершити програму. Щоб зрозуміти багатопоточність, краще мислити з погляду паралельності. Почнемо з чогось дуже простого :)Багатопоточність у Java: суть, «плюси» та часті пастки - 1Уяви, що твоя сім'я переїжджає з одного дому до іншого. Важлива частина переїзду – зібрати книжки. Книжок у вас накопичилося багато, і треба скласти їх у коробки. Зараз вільний лише ти. Мама готує їжу, брат збирає одяг, а сестра пішла у магазин. Поодинці ти сяк-так справляєшся, і, рано чи пізно, навіть сам виконаєш завдання, але часу знадобиться дуже багато. Втім, за 20 хвабон із магазину повернеться твоя сестра, та інших справ у неї немає. Тож вона може приєднатися до тебе. Завдання не змінювалося: скласти книги у коробки. Тільки виконується вдвічі швидше. Чому? Тому що робота робиться паралельно. Два різні «потоки» (ти і твоя сестра) одночасно виконують те саме завдання і, якщо нічого не зміниться, різниця в часі буде дуже великою в порівнянні з ситуацією, в якій ти б робив усе один.

Проблеми, які вирішує багатопоточність у Java

По суті, багатопоточність Java була придумана, щоб вирішити дві основні завдання:
  1. Одночасно виконувати кілька дій.

    У прикладі вище різні потоки (тобто члени сім'ї) паралельно виконували кілька дій: мабо посуд, ходабо в магазин, складали речі.

    Можна навести і більш «програмістський» приклад. Уяви, що в тебе є програма з інтерфейсом користувача. При натисканні кнопки "Продовжити" всередині програми повинні відбутися якісь обчислення, а користувач має побачити наступний екран інтерфейсу. Якщо ці дії здійснюються послідовно, після натискання кнопки "Продовжити" програма просто зависне. Користувач буде бачити той самий екран з кнопкою «Продовжити», поки всі внутрішні обчислення не будуть виконані, і програма не дійде до частини, де почнеться малювання інтерфейсу.

    Що ж, зачекаємо кілька хвабон!

    Багатопоточність у Java: суть, «плюси» та часті пастки - 3

    А ще ми можемо переробити нашу програму, або, як кажуть програмісти, розпаралелити. Нехай необхідні обчислення виконуються в одному потоці, а малювання інтерфейсу - в іншому. Більшість комп'ютерів вистачить цього ресурсів. У такому разі програма не буде «тупити», і користувач буде спокійно переходити між екранами інтерфейсу, не переймаючись тим, що відбувається всередині. Одне іншому не заважає :)

  2. Прискорити обчислення.

    Тут все набагато простіше. Якщо наш процесор має кілька ядер, а більшість процесорів зараз є багатоядерними, список наших завдань можуть паралельно вирішувати кілька ядер. Очевидно, що якщо нам потрібно вирішити 1000 завдань і кожна з них вирішується за секунду, одне ядро ​​впорається зі списком за 1000 секунд, два ядра — за 500 секунд, три — за 333 секунди і так далі.

Але, як ти вже читав у лекції, сучасні системи дуже розумні, і навіть на одному обчислювальному ядрі вони здатні реалізувати паралельність або псевдопаралельність, коли завдання виконуються поперемінно. Давай перейдемо від загальних речей до конкретних і познайомимося з головним класом у бібліотеці Java, що відноситься до багатопоточності, - java.lang.Thread. Власне, потоки в Java є екземплярами класу 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-1Thread-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 — ситуація, коли кілька потоків перебувають у стані очікування ресурсів, зайнятих одне одним, і жоден із них може продовжувати виконання. Ми ще поговоримо про нього в наступних лекціях, поки достатньо цього прикладу: Багатопоточність у Java: суть, «плюси» та часті пастки - 4 Уяви, що потік-1 працює з Об'єктом-1, а потік-2 працює з Об'єктом-2. При цьому програма написана так:
  1. Потік-1 перестане працювати з Об'єктом-1 і перейде на Об'єкт-2, як тільки Потік-2 перестане працювати з Об'єктом-2 і перейде на Об'єкт-1.
  2. Потік-2 перестане працювати з Об'єктом-2 і перейде на Об'єкт-1, як тільки Потік-1 перестане працювати з Об'єктом-1 і перейде на Об'єкт-2.
Навіть не маючи глибоких знань у багатопоточності ти легко зрозумієш, що нічого з цього не вийде. Потоки ніколи не зміняться місцями і чекатимуть один одного вічно. Помилка видається очевидною, але насправді це не так. Допустити її у програмі можна запитто. Ми розглянемо приклади коду, що викликає deadlock, у наступних лекціях. До речі, на Quora є чудовий приклад із реального життя , що пояснює що таке deadlock . «У деяких штатах Індії вам не продадуть землю сільськогосподарського призначення, якщо ви не зареєстровані як фермер. При цьому вас не зареєструють як фермер, якщо ви не володієте сільськогосподарськими землями». Здорово, що тут сказати! :) Тепер про race condition - стан гонки. Багатопоточність у Java: суть, «плюси» та часті пастки - 5Стан гонки — помилка проектування багатопоточної системи або програми, коли робота системи чи програми залежить від цього, у порядку виконуються частини коду. Згадай приклад із запуском потоків:
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 Веселий вийшов сценарій? :) А все тому, що робота нашої програми залежить від порядку виконання потоків. При найменшому порушенні послідовності наша кухня перетворюється на пекло, а божевільний робот трощить все навколо себе. Це також поширена проблема в багатопотоковому програмуванні, про яку ти ще не раз почуєш. На завершення лекції хочу порадити тобі книгу, присвячену багатопоточності.
Багатопоточність у Java: суть, «плюси» та часті пастки - 6
Java Concurrency in Practice написана ще в 2006 році, але не втратила актуальність. Вона присвячена багатопоточного програмування на Java, починаючи від основ і закінчуючи списком найпоширеніших помилок та антипатернів. Якщо колись вирішиш стати гуру багатопоточного програмування, ця книга є обов'язковою для прочитання. Побачимося на наступних лекціях! :)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ