1. введение
В этом уроке мы узнаем, как достичь мелкозернистой синхронизации, также известной как Чередование блокировок, шаблон для обработки параллельного доступа к структурам данных при сохранении хорошей производительности.
2. Проблема
HashMap не является потокобезопасной структурой данных из-за своей несинхронизированной природы. Это означает, что команды из многопоточной среды могут привести к несогласованности данных.
Чтобы преодолеть эту проблему, мы можем либо преобразовать исходную карту с помощью метода Collections#synchronizedMap , либо использовать структуру данных HashTable . Оба вернут потокобезопасную реализацию интерфейса Map , но они будут стоить производительности.
Подход определения исключительного доступа к структурам данных с одним объектом блокировки называется крупнозернистой синхронизацией .
В крупнозернистой реализации синхронизации каждый доступ к объекту должен осуществляться одновременно одним потоком. В итоге мы получаем последовательный доступ.
Наша цель-позволить параллельным потокам работать над структурой данных, обеспечивая при этом потокобезопасность.
3. Чередование замков
Чтобы достичь нашей цели, мы будем использовать шаблон чередования замков. Чередование блокировок – это метод, при котором блокировка происходит на нескольких сегментах или полосах, что означает, что доступ к сегменту блокирует только этот сегмент, а не всю структуру данных.
Есть несколько способов сделать это:
- Во – первых, мы могли бы использовать блокировку для каждой задачи, тем самым максимизируя параллелизм между задачами-однако это имеет более высокий объем памяти
- Или мы могли бы использовать одну блокировку для каждой задачи, которая использует меньше памяти, но также снижает производительность при параллелизме
Чтобы помочь нам справиться с этим компромиссом производительности и памяти, Guava поставляется с классом под названием Striped. Это похоже на логику , найденную в ConcurrentHashMap , но класс Striped идет еще дальше, уменьшая синхронизацию отдельных задач с помощью семафоров или реентерабельных блокировок.
4. Краткий Пример
Давайте сделаем краткий пример, чтобы помочь нам понять преимущества этого паттерна.
Мы сравним HashMap vs. ConcurrentHashMap и один замок против полосатого замка, что привело к четырем экспериментам.
Для каждого эксперимента мы будем выполнять параллельные операции чтения и записи на базовой карте . Что будет отличаться, так это то, как мы получаем доступ к каждому ведру.
И для этого мы создадим два класса – SingleLock и StripedLock. Это конкретные реализации абстрактного класса ConcurrentAccessExperiment , который выполняет эту работу.
4.1. Зависимости
Поскольку мы собираемся использовать класс Guava Striped , мы добавим зависимость guava :
com.google.guava guava 28.2-jre
4.2. Основной процесс
Наш Параллельный эксперимент доступа класс реализует поведение, описанное ранее:
public abstract class ConcurrentAccessExperiment { public final MapdoWork(Map map, int threads, int slots) { CompletableFuture>[] requests = new CompletableFuture>[threads * slots]; for (int i = 0; i < threads; i++) { requests[slots * i + 0] = CompletableFuture.supplyAsync(putSupplier(map, i)); requests[slots * i + 1] = CompletableFuture.supplyAsync(getSupplier(map, i)); requests[slots * i + 2] = CompletableFuture.supplyAsync(getSupplier(map, i)); requests[slots * i + 3] = CompletableFuture.supplyAsync(getSupplier(map, i)); } CompletableFuture.allOf(requests).join(); return map; } protected abstract Supplier> putSupplier(Map map, int key); protected abstract Supplier> getSupplier(Map map, int key); }
Важно отметить, что, поскольку наш тест привязан к процессору, мы ограничили количество сегментов несколькими доступными процессорами.
4.3. Параллельный доступ с помощью ReentrantLock
Теперь мы реализуем методы для наших асинхронных задач.
Наш класс Single Lock определяет единую блокировку для всей структуры данных с помощью ReentrantLock :
public class SingleLock extends ConcurrentAccessExperiment { ReentrantLock lock; public SingleLock() { lock = new ReentrantLock(); } protected Supplier> putSupplier(Mapmap, int key) { return (()-> { lock.lock(); try { return map.put("key" + key, "value" + key); } finally { lock.unlock(); } }); } protected Supplier> getSupplier(Map map, int key) { return (()-> { lock.lock(); try { return map.get("key" + key); } finally { lock.unlock(); } }); } }
4.4. Параллельный доступ с полосатым
Затем класс Striped Lock определяет полосатую блокировку для каждого ведра:
public class StripedLock extends ConcurrentAccessExperiment { Striped lock; public StripedLock(int buckets) { lock = Striped.lock(buckets); } protected Supplier> putSupplier(Mapmap, int key) { return (()-> { int bucket = key % stripedLock.size(); Lock lock = stripedLock.get(bucket); lock.lock(); try { return map.put("key" + key, "value" + key); } finally { lock.unlock(); } }); } protected Supplier> getSupplier(Map map, int key) { return (()-> { int bucket = key % stripedLock.size(); Lock lock = stripedLock.get(bucket); lock.lock(); try { return map.get("key" + key); } finally { lock.unlock(); } }); } }
Итак, какая стратегия работает лучше?
5. Результаты
Давайте используем JMH (жгут проводов Java Microbenchmark), чтобы выяснить это. Бенчмарки можно найти по ссылке на исходный код в конце учебника.
Запустив наш бенчмарк, мы можем увидеть нечто похожее на следующее (обратите внимание, что чем выше пропускная способность, тем лучше):
Benchmark Mode Cnt Score Error Units ConcurrentAccessBenchmark.singleLockConcurrentHashMap thrpt 10 0,059 ± 0,006 ops/ms ConcurrentAccessBenchmark.singleLockHashMap thrpt 10 0,061 ± 0,005 ops/ms ConcurrentAccessBenchmark.stripedLockConcurrentHashMap thrpt 10 0,065 ± 0,009 ops/ms ConcurrentAccessBenchmark.stripedLockHashMap thrpt 10 0,068 ± 0,008 ops/ms
6. Выводы
В этом уроке мы исследовали различные способы достижения лучшей производительности с помощью чередования блокировок в структурах типа Map . Мы создали бенчмарк для сравнения результатов с несколькими реализациями.
Из результатов наших тестов мы можем понять, как различные параллельные стратегии могут существенно повлиять на общий процесс. Полосатый шаблон блокировки дает значительное улучшение, так как он набирает ~10% больше как с помощью HashMap , так и с помощью ConcurrentHashMap .
Как обычно, исходный код этого учебника доступен на GitHub .