11 порад з багатопотокового програмування на Java


Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.

Розповідає Дж. Пол, автор блогу Java Revisited


Написання паралельного коду – непросте завдання, а перевірка його коректності – завдання ще складніше. Попри те, що Java надає велику підтримку багатопотоковості й синхронізації на рівні мови і API, виявляється, що написання коректного багатопотокового Java-коду залежить від досвіду і старанності конкретного програміста. Нижче викладені поради, які допоможуть Вам якісно підвищити рівень Вашого багатопотокового коду на Java. Деякі з Вас, можливо, вже ознайомлені з цими порадами, але корисно пригадати.

Багато з даних порад виробилися в процесі навчання і програмування, а також після прочитання книг “Java concurrency in practice” і “Effective Java”. Я раджу прочитати першу кожному Java-програмістові двічі; так, саме двічі. Паралелізм – заплутана і складна для розуміння тема (як, наприклад, для деяких – рекурсія), і після одноразового прочитання Ви можете не до кінця все зрозуміти.

Єдина мета використання паралелізму – створення масштабованих і швидких додатків, але при цьому завжди слід пам’ятати, що швидкість не повинна ставати перешкодою коректності. Ваша Java-програма повинна задовольняти своєму інваріанту незалежно від того, чи запущена вона в однопотоковому або багатопотоковому виді. Якщо Ви новачок у паралельному програмуванні, спершу ознайомтеся з різними проблемами, що виникають при паралельному запуску програм (наприклад: взаємне блокування, стан гонки, ресурсний голод і т. д.).

1. Використайте локальні змінні

public static class ConcurrentTask { private static List temp = Collections.synchronizedList (new ArrayList()); @Override public void execute (Message message) { // Використовуємо локальний тимчасовий список // List temp = new ArrayList ();
// Додамо в список щось із повідомлення temp.add ("message.getId()"); temp.add ("message.getCode()"); temp.clear (); // тепер можна переиспользовать }}

Проблема: Дані одного повідомлення потраплять в інше, якщо два виклики execute () “перетинаються”, тобто перший потік додасть id з першого повідомлення, потім другий потік додасть id з другого повідомлення (це станеться ще до очищення списку), таким чином дані одного з повідомлень будуть пошкоджені.

Варіанти розв’язань:

  1. Додати блок синхронізації у ту частину коду, де потік додає щось у тимчасовий список і очищує його. Таким чином, інший потік не зможе отримати доступ до списку, поки перший не закінчить роботу з ним. У такому разі ця частина коду буде однопотоковою, що знизить продуктивність додатка в цілому.
  2. Використати локальний список замість поля класу. Так, це збільшить витрати пам’яті, але Ви уникните блокування синхронізації і зробите код легко читаним. Також Вам не доведеться турбуватися про тимчасові об’єкти, оскільки про них потурбується збирач сміття.

Тут представлений тільки один з випадків, але при написанні паралельного коду особисто я віддаю перевагу локальним змінним над полями класу, якщо останніх не вимагає архітектура додатка.

2. Віддавайте перевагу незмінним класам над змінюваними

3. Скорочуйте області синхронізації

Будь-який код усередині області синхронізації не може бути виконаний паралельно, і якщо у Вашій програмі 5 % коду знаходиться у блоках синхронізації, то, згідно із законом Амдала, продуктивність усього додатка не може бути поліпшена більше як у 20 разів. Головна причина цього полягає в тому, що 5 % коду завжди виконується послідовно. Ви можете зменшити цю кількість, скорочуючи області синхронізації – спробуйте використати їх тільки для критичних секцій. Кращий приклад скорочення областей синхронізації – блокування з подвійною перевіркою, яку можна реалізувати в Java 1.5 і вище за допомогою volatile змінних.

4. Використайте пул потоків

Створення потоку (Thread) – дорога операція. Якщо Ви хочете створити масштабований Java-додаток, то Вам треба використати пул потоків. Крім ваговитості операції створення, управління потоком вручну породжує багато повторюваного коду, який, перемішуючись із бізнес-логікою, зменшує читаність коду в цілому. Управління потоками – завдання фреймворка, будь то інструмент Java або якийсь інший, що Ви захочете використати. У JDK є добре організований, багатий і повністю протестований фреймворк, відомий як Executor framework, який можна використати скрізь, де знадобиться пул потоків.

5. Використайте утиліти синхронізації замість wait () і notify ()

У Java 1.5 з’явилося багато утиліт синхронізації, таких як CyclicBarrier, CountDownLatch і Semaphore. Вам завжди слід спочатку вивчити, що є в JDK для синхронізації, до того як використати wait () і notify (). Буде набагато простіше реалізувати шаблон читач-письменник за допомогою BlockingQueue, ніж через wait () і notify (). Також набагато простіше буде зачекати на 5 потоків для завершення обчислень, використовуючи CountDownLatch, ніж реалізовувати те саме wait () і notify (). Вивчіть пакет java.util.concurrent, щоб писати паралельний код на Java якнайкраще.

6. Використайте BlockingQueue для реалізації Producer – Consumer

Ця порада логічно слідує з попередньої, але я виділив її окремо, зважаючи на її важливість для паралельних додатків, використовуваних у реальному світі. Розв’язання проблем багатопотоковості ґрунтується на шаблоні Producer-Consumer, і BlockingQueue – кращий спосіб реалізації його в Java. На відміну від Exchanger, який можна бути використати у разі одного письменника і читача, BlockingQueue може бути використана для правильної обробки декількох письменників і читачів.

7. Використайте потокобезпечні колекції замість колекцій з блокуванням доступу

Потокобезпечні колекції дають більшу масштабованість і продуктивність, ніж їх аналоги з блокуванням доступу (Collections.synchronizedCollection та ін.). СoncurrentHashMap, яка, на мою думку, є найпопулярнішою потокобезпечною колекцією, демострує кращу продуктивність, ніж блокувальні HashMap або Hashtable, у разі, коли кількість читачів перевершує кількість письменників. Інша перевага потокобезпечих колекцій полягає в тому, що вони реалізовані за допомогою нового механізму блокування (java.util.concurrent.locks.Lock) і використовують нативні механізми сихнронізації, надані апаратним забезпеченням і JVM. Використайте CopyOnWriteArrayList замість Collections.synchronizedList, якщо читання зі списку відбувається частіше, ніж його зміна.

8. Використайте семафори для створення обмежень

Щоб створити надійну і стабільну систему, у Вас мають бути обмеження на ресурси (бази даних, файлову систему, сокети і так далі). Ваш код у жодному разі не повинен створювати та/або використовувати велику кількість ресурсів. Семафори (java.util.concurrent.Semaphore) – гарний вибір для створення обмежень на використання дорогих ресурсів, таких як підключення до бази даних (до речі, в цьому випадку можна використати пул підключень). Семафори допоможуть створити обмеження і заблокують потоки у разі недоступності ресурсу.

9. Використайте блоки синхронізації замість блокованих методів

Дана порада розширює пораду зі скорочення областей синхронізації. Використання блоків синхронізації – один з методів скорочення області синхронізації, що також дозволяє виконати блокування на об’єкті, відмінному від поточного, представленого покажчиком this. Першим кандитатом має бути атомарна змінна, потім volatile змінна, якщо вони задовольняють Вашим вимогам до синхронізації. Якщо Вам потрібно взаємний виняток, використайте в першу чергу ReentrantLock, або блок synchronized. Якщо Ви новачок у паралельному програмуванні, і не розробляєте якийсь життєво важливий додаток, то можете просто використати блок synchronized – так буде безпечніше і простіше.

10. Уникайте використання статичних змінних

Як показано в першій пораді, статичні змінні, будучи використаними в паралельному коді, можуть спричинити багато проблем. Якщо Ви все ж використаєте статичну змінну, то переконайтеся, що це константа або незмінна колекція. Якщо Ви думаєте про те, щоб перевикористати колекцію з метою економії пам’яті, поверніться до першої поради.

11. Використайте Lock замість synchronized

Остання порада, яку слід використовувати обережно. Інтерфейс Lock – потужний інструмент, але його сила веде до великої відповідальності. Різні об’єкти Lock на операції читання і запису дозволяють реалізовувати масштабовані структури даних, такі як ConcurrentHashMap, але при цьому вимагають великої обережності при своєму програмуванні. На відміну від блоку synchronized, потік не звільняє блокування автоматично. Вам доведеться явно викликати unlock (), щоб зняти блокування. Гарною практикою є виклик цього методу у блоці finally, щоб блокування завершувалося за будь-яких умов:

lock.lock ();try { //do something ...} finally { lock.unlock ();}

Висновок

Ви ознайомились із порадами з написання багатопотокового коду на Java. Ще раз повторюся, не зашкодить перечитувати “Java concurrency in practice” і “Effective Java” час від часу. Також можна виробляти потрібний для паралельного програмування спосіб мислення, просто читаючи чужий код і намагаючись візуалізувати проблеми під час розробки. Насамкінець запитайте себе, яких правил Ви дотримуєтеся, коли розробляєте багатопотокові додатки на Java?

Переклад статті Top 10 Java Multithreading and Concurrency Best Practices

Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?


Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *