Привіт! Перш за все, вітаю: ти дійшов до теми Багатопоточність у 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("Я потік! Моє ім'я " + 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 — ситуація, при якій кілька потоків знаходяться в стані очікування ресурсів, зайнятих один одним, і жоден з них не може продовжувати виконання. Ми ще поговоримо про нього у наступних лекціях, поки достатньо цього прикладу: Багатопоточність у 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, починаючи з основ і закінчуючи списком найпоширеніших помилок і антипатернів. Якщо коли-небудь вирішиш стати гуру багатопоточного програмування, ця книга обов'язкова до прочитання. Побачимось на наступних лекціях! :)