
— Привет, Амиго!
— Привет, Ким!
— Я тебе сегодня расскажу про «канкаренси» — 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 есть очень много преимуществ. Просто надо очень хорошо разобраться в этих классах, чтобы их применять.
— Ясно. Спасибо, Ким. Действительно, очень интересные классы. Надеюсь, когда-нибудь я тоже буду виртуозно ими владеть.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ