Рубрики
Без рубрики

Введение в чередование замков

Узнайте о ключевых различиях между крупнозернистой синхронизацией и мелкозернистой синхронизацией и о том, как реализовать их в Java.

Автор оригинала: baeldung.

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 Map doWork(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(Map map, 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(Map map, 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 .