— Привіт, Аміго!
— Привіт Кім!
— Я тобі сьогодні розповім про "канкаренсі" — Concurrency.
Concurrency - це бібліотека класів в Java, в якій зібрали спеціальні класи, оптимізовані для роботи з декількох ниток. Ця тема дуже цікава та велика. Але сьогодні ми просто познайомимося із нею. Ці класи зібрані в пакеті java.util.concurrent. Я розповім про кілька цікавих класів.
Атомарні типи.
Ти вже знаєш, що навіть операція count++ не є безпечною (thread-safe). При збільшенні змінної на 1 реально відбувається три операції, внаслідок чого може виникнути конфлікт за одночасної зміни цієї змінної.
— Ага, Еллі розповідала це трохи раніше:
Нитка 1 | Нитка 2 | Результат |
---|---|---|
|
|
|
— Саме. Тоді Java були додані типи даних, які виконують такі операції нерозривно – атомарно. (Атом — неподільний).
Так у Java з'явилися типи AtomicInteger, AtomicBoolean, AtomicDouble і т.д.
От, припустімо, нам потрібно зробити клас «лічильник»:
class Counter
{
private int c = 0;
public void increment()
{
c++;
}
public void decrement()
{
c--;
}
public int value()
{
return c;
}
}
Як би ти зробив його об'єкти thread-safe?
— Так, зробив би всі методи synchronized і все:
class synchronized Counter
{
private int c = 0;
public synchronized void increment()
{
c++;
}
public synchronized void decrement()
{
c--;
}
public synchronized int value()
{
return c;
}
}
— Відмінна робота. А от як би він виглядав з використанням атомарних типів:
class AtomicCounter
{
private AtomicInteger c = new AtomicInteger(0);
public void increment()
{
c.incrementAndGet();
}
public void decrement()
{
c.decrementAndGet();
}
public int value()
{
return c.get();
}
}
І твій і мій класи працюють однаково, але клас з AtomicInteger працює швидше.
— Тобто. різниця невелика?
Так. Зі свого досвіду я можу порадити завжди в лоб використовувати synchronized. І тільки коли весь код програми вже написано і починається процес його оптимізації, то можна починати переписувати його частини з використанням Atomic-типів. Але в будь-якому разі я хотіла б, щоб ти знав, що такі типи є. Навіть якщо не активно ними користуватимешся, ти завжди можеш побачити код, написаний з їх застосуванням.
— Згоден, у цьому є сенс.
— До речі, а ти помітив, що атомарні типи – не immutable? AtomicInteger, на відміну від просто Integer, містить методи зміни свого внутрішнього стану.
— Зрозуміло, прямо як String та StringBuffer.
— Так, щось на кшталт того.
Нітебезпечні (іноді називають — потокобезпечні) колекції.
В якості такої колекції я хотіла б навести ConcurrentHashMap. Як зробити HashMap thread-safe?
— Зробити всі його методи synchronized?
— Так, але уяви тепер, що в тебе є один такий SynchronizedHashMap, а до нього звертаються десятки ниток. І сто разів на секунду до цього map додається новий запис, при цьому весь об'єкт блокується для читання та запису.
— Так, але це стандартний підхід. Що можна зробити?
— Розробники Java вигадали кілька крутих штук.
По-перше, вони зберігають дані в ConcurrentHashMap не одним шматком, а розбиваючи їх на порції — "Кошики". І коли хтось змінює дані в ConcurrentHashMap, то блокується не весь об'єкт, а лише один кошик, до якого відбувається доступ. Тобто. насправді об'єкт може одночасно змінювати багато ниток.
По-друге, пам'ятаєш проблему, що не можна одночасно йти елементами списку/мепа і змінювати його? Такий код кине виняток:
HashMap<String, Integer> map = New HashMap<String, Integer>();
for (String key: map.keySet())
{
if (map.get(key)==0)
map.remove(key);
}
А в ConcurrentHashMap це можна:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
for (String key: map.keySet())
{
if (map.get(key)==0)
map.remove(key);
}
У Concurrency є дуже багато переваг. Просто треба добре розібратися в цих класах, щоб їх застосовувати.
— Зрозуміло. Дякую, Кім. Справді, дуже цікаві класи. Сподіваюся, колись я теж віртуозно ними володітиму.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ