У цій статті я торкнуся велику і складну тему багатопоточності в Java. Зрозуміло, про все в одній статті я не зможу розповісти, тому торкнуся лише самі основні теми.
переваги многопоточности
Не дивлячись на безліч проблем, многопоточность дуже корисна в призначених для користувача інтерфейсів. Поки один потік займається обробкою даних, другий спокійнісінько отрісовиваєт графіком. В результаті ми бачимо на екрані плавну анімацію і можемо взаємодіяти з елементами інтерфейсу, не побоюючись, що все зависне.
Також многопоточность дозволяє поліпшити швидкість обробки даних: поки один потік готує дані, наприклад, викачуючи їх з інтернету дрібними порціями, другий і третій потоки можуть їх обробляти, а четвертий записувати результат на диск.
Якщо ж у нас багатоядерний процесор, то потоки дозволять істотно поліпшити продуктивність, переклавши роботу на інші ядра.
Що ж, досить теорії, перейдемо до практики.
створення потоків
В Java для роботи з потоками служить клас Thread. Є два способи створення нового потоку, але їх можна записати куди більшим числом способів.
Перший варіант, це розширення класу Thread.
public class CustomThread extends Thread # 123;
System. out. println # 40; 6 # 41; ;
Можна ще побавитися з Reflection API, але це вже якось самі.
переривання потоків
Так-так, саме переривання, а не зупинка або пауза. Потоки не можна зупинити стандартними засобами, тому що з цим пов'язано безліч проблем. Тому методи stop, suspend, pause і destroy в класі Thread позначені як deprecated. Так що зупинку доводиться робити самому.
Взагалі, потік в Java працює до тих пір, поки виконується код в його методі run. Якщо в run буде проста операція, скажімо, висновок тексту в консоль, то потік відразу ж завершиться після цієї операції. Якщо ж ми в потоці записуємо дані в циклі в 100 файлів, то потік буде працювати до тих пір, поки не виконає своє завдання.
І, до речі, якщо потік працює в фоні і ви закриваєте програму, то процес цієї програми все одно залишиться висіти в тлі, поки не завершаться всі потоки. Щоб по закінченню програми гарантовано отримати завершення потоків, потрібно зробити з потоку демона. Звучить смішно, але так і є:
thread. setDaemon # 40; true # 41;
Отже, у нас є потік, який виконує в циклі якусь повторювану операцію і нам потрібно в певний момент часу його завершити. Найбільш очевидний спосіб, це завести логічну змінну і перевіряти її стан. Найчастіше роблять так:
private boolean isRunning;
All threads finished. Counter = 5000
Як це буде працювати:
- Ми створили два потоки і запустили їх.
- Припустимо, перший потік запустився швидше і увійшов в цикл while. Другий поки запускається.
- Перший потік бачить блок synchronized. Виконується перевірка - чи немає зараз в цьому блоці інших потоків? Ні, тому перший потік заходить в блок. Другий поки що увійшов в цикл while.
- Перший потік зараз в циклі for збільшує лічильник. Другий потік доходить до блоку synchronized. Знову виконується перевірка і оскільки потік всередині є, дозвіл увійти всередину ніхто не почув, а значить другий потік чекає.
- Перший потік все ще в циклі for. Другий потік все так же чекає.
- Нарешті, перший потік виходить з циклу for і залишає область синхронізації. Другий потік отримує дозвіл увійти всередину.
Таким чином, виходить синхронізована робота потоків.
Блоки синхронізації слід розставляти з розумом. Якби ми зробили ось так:
private static void printCounter # 40; # 41; # 123;
synchronized # 40; Counter. class # 41; # 123;
while # 40; Counter. get # 40; # 41; <5000 ) {
for # 40; int i = 0; i <10 ; i ++ ) {
Counter. increment # 40; # 41; ;
Thread. sleep # 40; 1 # 41; ;
# 125; catch # 40; InterruptedException ex # 41; # 123;
Thread. currentThread # 40; # 41 ;. interrupt # 40; # 41; ;
ми б теж отримали вірний результат 5000, ось тільки працював би у нас тільки один потік:
- Створюємо два потоки і запускаємо їх.
- Припустимо, перший потік запустився швидше і увійшов в блок синхронізації. Другий поки запускається.
- Перший потік тепер в циклі while. Другий потік зустрів блок synchonized і не отримав дозвіл увійти.
- Перший потік працює. Другий чекає.
- Через деякий кількість часу, перший потік збільшив лічильник до 5000 і вийшов з циклів і блоку синхронізації. Другому потоку дозволяється увійти всередину.
- Перший потік завершив роботу. Другий потік перевірив, що умова Counter.get () <5000 уже не выполняется и не вошёл в цикл while. Покинул блок синхронизации и завершился.
Інший варіант вирішення проблеми з лічильником - зробити його методи get і increment синхронізованими. Тоді блок синхронізації в методі run не знадобиться.
public class SynchronizedCounter # 123;
private static int counter = 0;
Тобто ми заходимо в блок синхронізації, монітор. Повертаємо значення і виходимо з монітора. Якщо під час критичної секції відбулося виключення, ми також виходимо з монітора.
У байткод synchronized методу інструкцій monitorenter і monitorexit не було, але це не означає, що немає входу в монітор. Прапор SYNCHRONIZED у методу говорить JVM про те, що всі ці інструкції потрібно виконати. Тобто, вони не з'являються в коді, але приховані в JVM - вона все одно їх виконає.
Забігаючи вперед, продемонструю ще одне можливе рішення проблеми. У пакеті java.util.concurrent є безліч класів для різних багатопотокових потреб. Одним з таких класів є AtomicInteger. який робить операції над числами атомарними.
public class AtomicCounter # 123;
private static final AtomicInteger counter = new AtomicInteger # 40; # 41; ;
public static int get # 40; # 41; # 123;
return counter. get # 40; # 41; ;
public static void increment # 40; # 41; # 123;
counter. incrementAndGet # 40; # 41; ;
Тепер ніде не потрібно додавати блок синхронізації.
Про інших класах, що спрощують роботу з багатопоточність, я постараюся розповісти в наступній статті.
Синхронізація. Приклад проектування многопоточного додатки
На довершення статті, хочу показати приклад невеликого додатки і важливість правильного проектування потоків.
Припустимо, у нас є 20 файлів з даними. Нам потрібно найбільш ефективно прочитати ці файли і вивести дані на екран. Дані, у міру читання, будуть додаватися до списку і вже звідти виводитися на екран.
Є клас, який відповідає за панель малювання, туди ми будемо додавати об'єкти у міру читання:
private PaintPanel panel;
private void processFile # 40; File file # 41; # 123;
System. out. printf # 40; "Process file% s in% s thread \ n". file. getName # 40; # 41 ;. Thread. currentThread # 40; # 41 ;. getName # 40; # 41; # 41; ;
try # 40; FileInputStream fis = new FileInputStream # 40; file # 41; ;
DataInputStream dis = new DataInputStream # 40; fis # 41; # 41; # 123;
while # 40; dis. available # 40; # 41;> 0 # 41; # 123;
Triangle triangle = Triangle. read # 40; dis # 41; ;
panel. addTriangle # 40; triangle # 41; ;
Thread. sleep # 40; 1 # 41; ;
# 125; catch # 40; IOException | InterruptedException ie # 41; # 123;
Тепер помилок немає, але обробка 20 файлів займає близько п'яти хвилин. Причому, перші файли читаються швидко, а потім робота сповільнюється. Спробуйте зрозуміти, чому так відбувається.
▌ Варіант 2. Один файл - один потік
Маючи 20 потоків логічно припустити, що робота пройде в 20 разів швидше. Так би воно і було, май наш процесор хоча б 20 ядер. В іншому випадку ми лише створимо додаткове навантаження і всі наші дані можуть отрісовиваться не надто плавно.
List
for # 40; File file. files # 41; # 123;
Thread thread = new Thread # 40; # 40; # 41; -> processFile # 40; file # 41; # 41; ;
threads. add # 40; thread # 41; ;
thread. start # 40; # 41; ;
for # 40; Thread thread. threads # 41; # 123;
thread. join # 40; # 41; ;
# 125; catch # 40; InterruptedException e # 41; # 123;
Thread. currentThread # 40; # 41 ;. interrupt # 40; # 41; ;
Проте, час роботи тепер: 40 секунд. Це довго. Та ж проблема, що уповільнює роботу в першому варіанті, уповільнює все і зараз. Повинен бути спосіб позбутися від synchronized блоків і такий спосіб є.
▌ Варіант 3. Використання синхронізованого списку
Для того, щоб зробити зі звичайного списку синхронізований, викликаємо метод
Collections. synchronizedList # 40; list # 41;
Тепер ми отримаємо список, у якого всі методи будуть synchronized. Але, на жаль, итератор, який використовується в foreach таким чином не вийде синхронізувати і нам доведеться або обертати його в synchronized блок, або відмовитися від нього на користь простого перебору елементів в циклі for.
public class PaintPanelSynchronizedList extends PaintPanel # 123;
private final List
public PaintPanelSynchronizedList # 40; int width, int height # 41; # 123;
super # 40; width, height # 41; ;
triangles = Collections. synchronizedList # 40; new ArrayList <> # 40; # 41; # 41; ;
▌ Варіант 4. Обмеження кількості потоків
Ми можемо обмежити кількість потоків, створивши пул:
ExecutorService es = Executors. newFixedThreadPool # 40; maxThreads # 41; ;
Тепер можна створити хоч сотню потоків, виконуватися в один момент часу будуть не більше maxThreads потоків.
Обмежимо пул п'ятьма потоками
ExecutorService es = Executors. newFixedThreadPool # 40; 5 # 41; ;
for # 40; File file. files # 41; # 123;
es. execute # 40; # 40; # 41; -> processFile # 40; file # 41; # 41; ;
es. shutdown # 40; # 41; ;
es. awaitTermination # 40; 2. TimeUnit. MINUTES # 41; ;
# 125; catch # 40; InterruptedException e # 41; # 123;
Thread. currentThread # 40; # 41 ;. interrupt # 40; # 41; ;
Тепер час виконання збільшилася до 11 секунд, але зате отрисовка проходить плавніше і ми можемо бути впевнені, що будь у нас сотня файлів, система не отримає велике навантаження.
Кращою практикою, є обмеження пулу потоків за кількістю процесорів в системі:
Executors. newFixedThreadPool # 40; Runtime. getRuntime # 40; # 41 ;. availableProcessors # 40; # 41; # 41; ;
▌ Варіант 5. java.util.concurrent