undefined

Канкаренси, BlockingQueues (java7)

Java Multithreading
6 уровень , 9 лекция
Открыта
Канкаренси, BlockingQueues (java7) - 1

— Привет, Амиго!

— Привет, Ким!

— Я тебе сегодня расскажу про «канкаренси» — Concurrency.

Concurrency – это библиотека классов в Java, в которой собрали специальные классы, оптимизированные для работы из нескольких нитей. Эта тема очень интересная и обширная. Но сегодня мы просто познакомимся с ней. Эти классы собраны в пакете java.util.concurrent. Я расскажу про пару интересных классов.

Атомарные типы.

Ты уже знаешь, что даже операция count++ не является потокобезопасной (thread-safe). При увеличении переменной на 1 реально происходит три операции, в результате чего может возникнуть конфликт при одновременном изменении этой переменной.

— Ага, Элли рассказывала это немного раньше:

Нить 1 Нить 2 Результат
register1 = count;
register1++;
count = register1;
register2 = count;
register2++;
count = register2;
register1 = count;
register2 = count;
register2++;
count = register2;
register1++;
count = register1;

— Именно. Тогда в 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 есть очень много преимуществ. Просто надо очень хорошо разобраться в этих классах, чтобы их применять.

— Ясно. Спасибо, Ким. Действительно, очень интересные классы. Надеюсь, когда-нибудь я тоже буду виртуозно ими владеть.

Комментарии (71)
Чтобы просмотреть все комментарии или оставить свой,
перейдите в полную версию
Алексей 35 уровень, Красноярск
13 апреля 2021
Поправьте, если я неправильно понимаю. HashMap представляет собой Set элементов (корзин), каждый из которых, в свою очередь, является LinkedList - ом из двух элементов (ключ и значение). Сортируются корзины внутри сета по хеш коду нулевых значений линкедлистов (ключей). Т.е. HashMap - это по сути HashSet из LinkedList-ов. Объект HashMap имеет один объект мьютекс и блокируется вся при совместном доступе. А ConcurrentHashMap - это тот же HashMap, только он дополнительно хранит в себе массив из объектов, которые используются для блокировки доступа к корзинам. Только вот размер этого массива не равен размеру мапы. Назначение объектов-блокираторов для каждой корзины было бы слишком ресурсно затратно. Поэтому мапа разбивается на блоки из некоего количества корзин, и каждому такому блоку назначается один объект из массива объектов для блокировки доступа. Т.е. возможны ситуации, когда несколько нитей обратятся одновременно к объектам мапы находящимся "под одним" блокиратором и встанут в очередь на доступ. И нельзя гарантировать, что, например, две нити могут одновременно получить доступ к двум соседним элементам мапы. Эти два элемента могут быть под одним мьютексом. Но я не могу понять, почему обычную мапу нельзя изменять в цикле, а ConcurrentHashMap можно.
Альфир Нуркаев 28 уровень, Пермь
21 марта 2021
Snesh'no
SolomonVP 31 уровень, Томск-Москва-Краснодар
22 февраля 2021
Во! Эта тема уже интересная... вчера на тестовом(с HeadHunter) было задание что то там про атомарность. :) Ответить не смог, так как не помнил проходили мы в Java это или нет, отвечать с помощью гугла не стал - это же ложь вроде как получится. Примерно накидал ответов какие подходили под понятие атомарность из БД. А тут с утра эта тема. :)
Иван 31 уровень, Москва
24 января 2021
ВАУ! Я как раз искал такую штуку, чтобы хранить игровые объекты и сделать многопоточную игру!
Max Pankov 31 уровень, Москва
20 января 2021
Помню, спрашивали - назовите способы создания потокобезопасных мап. 1) Collections.synchronizedMap() 2) HashTable 3) ConcurrentHashMap Так вот тема полезная. Мало того, что HashTable устаревший, так еще и важная вещь из этого урока, что 1 и 2 блокируются полностью, а 3 работает по методике, описанной в этой теме, что увеличивает производительность.
Agent Smith 37 уровень
5 декабря 2020
И когда кто-то меняет данные в ConcurrentHashMap, то блокируется не весь объект, а только одна корзина, к которой происходит доступ. Т.е. на самом деле объект могут одновременно менять много нитей. Последняя задача в предыдущем уроке была как раз об этом. 😉
Vladimir “Rain_Senpai1995” Soldatenko 35 уровень, Киев
8 ноября 2020
Библиотека Concurrency не ограничивается атомарными типами и ConcurrentHashMap, есть и BlockingQueues и механизмы типа CountdownLatch, прочее ... Хотелось бы больше инфы, а не галопом по Европах... UPD: Оставлю это здесь: Advanced Java Concurrency
Avrelio 38 уровень, Aveiro
5 ноября 2020
— Понятно, прямо как String и StringBuffer. тут походу опечатка не String a StringBuilder
barracuda 41 уровень, Санкт-Петербург Expert
28 августа 2020
на миллионе операций AtomicInteger + incrementAndGet() у меня разница с int + synchronized блок = 33 миллисекунды. 440 миллисекунд на миллион операций int + synchronized блок, 407 миллисекунд на миллион операций AtomicInteger + incrementAndGet().
barracuda 41 уровень, Санкт-Петербург Expert
28 августа 2020
А разве можно писать как в примере в этой статье class synchronized Counter {... ??