Почему ConcurrentHashMap не может иметь блокировку для каждого ведра?

Как мы знаем, java ConcurrentHashMap имеет ряд внутренних блокировок, и каждый из них защищает некоторую область массива ведра.

Вопрос: почему мы не можем создать блокировку для каждого ведра?

Уже был задан подобный вопрос: Недостаток увеличения количества разделов в Java ConcurrentHashMap?

В соответствии с ответом есть несколько причин:

  • максимальное число потоков, работающих одновременно, ограничено количеством ядер процессора. Это правильно? Можем ли мы ВСЕГДА сказать, что если у нас есть 8-ядерный процессор, нам не нужно больше 8 заблокированных областей в ConcurrentHashMap?

  • Отходы кэша L2 теряются. Почему?

  • Существует пустая память. Похоже, это связано с дополнительным созданием блокировки.

Есть ли еще причины?

Ответы

Ответ 1

Можем ли мы ВСЕГДА утверждать, что если у нас есть 8-ядерный процессор, нам не нужно больше 8 заблокированных областей в ConcurrentHashMap?

Нет, это совершенно неправильно. Это зависит от двух факторов: количества потоков (concurrency) и количества сегментных столкновений. Если два потока конкурируют за один и тот же сегмент, один поток может блокировать другой.

В то время как у вас может быть только столько потоков, которым принадлежит ядро, так как у вас есть ядра, большая ошибка с вышеприведенным утверждением заключается в том, чтобы предположить, что поток, который не работает на ядре, не имеет блокировки. Но поток, владеющий блокировкой, все еще может потерять CPU на коммутаторе задачи для следующего потока, который затем блокируется при попытке получить тот же замок.

Но его необычно настраивать количество потоков на количество ядер, особенно для интенсивных вычислительных задач. Таким образом, уровень concurrency a ConcurrentHashMap косвенно зависит от количества ядер в типичных настройках.


Наличие блокировки для каждого ведра подразумевает сохранение состояния блокировки и очереди ожидания для каждого ведра, что означает довольно много ресурсов. Имейте в виду, что блокировка требуется только для одновременных операций записи, но не для потоков чтения.

Однако для реализации Java 8 это соображение устарело. Он использует алгоритм без ожидания для обновления ведра, по крайней мере для ковшей без столкновений. Это немного похоже на блокировку на каждый ведро, поскольку потоки, работающие в разных ковшиках, не мешают друг другу, но без накладных расходов на поддержание состояния блокировки и очереди ожидания. Единственное, на что нужно обратить внимание, - дать карте соответствующий начальный размер. Следовательно, concurrencyLevel, если задано, используется в качестве начального подсказки по размеру, но в противном случае игнорируется.

Ответ 2

Надеюсь, я делаю достойную работу по разъяснению... вроде бы бросился в данный момент...

Ответ на ваш первый вопрос:

"почему мы не можем создать блокировку для каждого ведра?"

Можно ли создать блокировку для каждого ведра - это не обязательно лучший способ действий.

Ответ на ваш вопрос:

"Можем ли мы ВСЕГДА утверждать, что если у нас есть 8-ядерный процессор, нам не нужно больше 8 заблокированных областей в ConcurrentHashMap"

технически "Нет", хотя это зависит от того, что вы подразумеваете под "необходимостью". Наличие нескольких регионов, которые соответствуют вашей системе максимум concurrency или немного больше, не обязательно препятствует конкуренции, но на практике это работает очень хорошо. Ничто не мешает двум потокам пытаться получить доступ к одной и той же области одновременно, даже если есть другие регионы, которые не заблокированы.

То, что вы можете гарантировать, имея 8 регионов или более на 8-ядерном процессоре, заключается в том, что к любым регионам можно одновременно обращаться без конкуренции. Если у вас 8 ядер (не Hyper Threaded), вы можете выполнить не более 8 операций одновременно. Даже тогда идеальное количество регионов может быть больше (скажем, 16), чем количество ядер, потому что это сделает конкуренцию менее вероятной при низкой стоимости (всего 8 дополнительных блокировок).

Преимущество наличия дополнительных областей в конечном итоге уменьшается по мере увеличения числа регионов относительно вашего максимального concurrency, что приводит к тому, что они являются пустой тратой пространства (памяти), как указано в JavaDoc. Это баланс между вероятностью конкуренции (с учетом блокировки на одном регионе, какой будет вероятность того, что другой поток попытается получить к ней доступ) и потерянного пространства.

Есть несколько других факторов, которые повлияют на производительность ConcurrentHashMap:

  • Время выполнения заблокированного кода - хорошая практика сделать блокированные секции кода небольшими, чтобы они быстро заполнили и отпустили свои блокировки. Чем быстрее блокировки освобождаются, тем быстрее разрешается конфликт.
  • Распределение данных. Удобно распределенные данные имеют тенденцию работать лучше при высоком уровне concurrency. Наличие всех ваших данных, сгруппированных в одном регионе, означает, что вы всегда будете сталкиваться с конфликтом.
  • Схема доступа к данным. Одновременный доступ к различным областям данных будет работать лучше, поскольку ваши потоки не будут бороться за блокировки ресурсов. Наличие хорошо распределенных данных не имеет значения, если вы только пытаетесь получить доступ к одному региону за раз.

Независимо от того, сколько регионов существует, все три из этих факторов могут положительно или отрицательно повлиять на производительность и могут сделать число регионов менее актуальными. Поскольку они играют большую роль, они делают менее вероятным, что значительно большее количество регионов поможет вам в целом. Так как вы можете выполнять только так много потоков одновременно, то потоки, которые быстро завершают работу и освобождают свои блокировки, лучше фокусируются.

Что касается вашего вопроса о кеше: я честно не уверен, но я могу предположить. Когда вы сильно используете карту, эти блокировки попадают в кеш и занимают место, что потенциально ударяет по другим вещам, которые могут быть более полезными. Кэш гораздо более дефицитный, чем основная память, а кэш-память пропускает много времени. Я думаю, что идея здесь - это общее отвращение к тому, чтобы вкладывать в кеш много вещей, которые не приносят значительной пользы. Доведено до крайности: если кеш заполнен замками (как-то), и каждый вызов данных выходит в память, вы снижаете производительность.