Ленько инициализировать карту Java в потоковом режиме

Мне нужно лениво инициализировать карту и ее содержимое. У меня есть код ниже:

class SomeClass {
    private Map<String, String> someMap = null;

    public String getValue(String key) {
        if (someMap == null) {
            synchronized(someMap) {
                someMap = new HashMap<String, String>();
                // initialize the map contents by loading some data from the database.
                // possible for the map to be empty after this.
            }
        }
        return someMap.get(key);  // the key might not exist even after initialization
    }
}

Это, очевидно, не является потокобезопасным, как если бы один поток появился, когда someMap имеет значение null, продолжает инициализировать поле до new HashMap, и пока он все еще загружает данные на карте, другой поток выполняет getValue и не получает данные, если они могли существовать.

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

Обратите внимание, что возможно, что key не будет на карте после инициализации. Кроме того, возможно, что после инициализации карта просто пуста.

Ответ 1

Двойная проверка блокировки

Двойная проверка блокировки требует выполнения нескольких шагов для правильной работы, вам не хватает двух из них.

Сначала вам нужно сделать someMap в переменной volatile. Это значит, что другие потоки будут видеть изменения, внесенные в него, когда они будут сделаны, но после завершения изменений.

private volatile Map<String, String> someMap = null;

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

    if (someMap == null) {
        synchronized(this) {
            if (someMap == null) {

Не назначать до готовности к использованию

В своем генерации постройте карту в переменной temp, затем назначьте ее в конце.

                Map<String, String> tmpMap = new HashMap<String, String>();
                // initialize the map contents by loading some data from the database.
                // possible for the map to be empty after this.
                someMap = tmpMap;
            }
        }
    }
    return someMap.get(key); 

Чтобы объяснить, почему требуется временная карта. Как только вы закончите строку someMap = new HashMap..., тогда someMap больше не равно null. Это означает, что другие вызовы get будут видеть его и никогда не будут пытаться войти в блок synchronized. Затем они попытаются перейти с карты, не дожидаясь завершения вызовов базы данных.

Убедитесь, что назначение someMap - это последний шаг в синхронизированном блоке, который предотвращает это.

unmodifiableMap

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

            someMap = Collections.unmodifiableMap(tmpMap);

Почему бы не использовать ConcurrentMap?

ConcurrentMap делает отдельные действия (т.е. putIfAbsent) потокобезопасными, но он не отвечает фундаментальному требованию здесь, ожидая, пока карта полностью заполнена данными, прежде чем разрешить чтение из нее.

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

Зачем синхронизировать это?

Нет причин.:) Это был просто самый простой способ представить действительный ответ на этот вопрос.

Конечно, лучше было бы синхронизировать на частном внутреннем объекте. У вас улучшена инкапсуляция, которая была продана для незначительно увеличенного использования памяти и времени создания объекта. Основной риск с синхронизацией на this заключается в том, что он позволяет другим программистам получить доступ к вашему объекту блокировки и, возможно, попытаться синхронизировать его самостоятельно. Это приводит к нежелательному конфликту между их обновлениями и вашим, поэтому внутренний объект блокировки безопаснее.

В действительности, хотя отдельный объект блокировки во многих случаях является излишним. Это решение, основанное на сложности вашего класса и том, как широко используется против простоты блокировки на this. Если вы сомневаетесь, вы, вероятно, должны использовать внутренний объект блокировки и использовать самый безопасный маршрут.

В классе:

private final Object lock = new Object();

В методе:

synchronized(lock) {

Что касается объектов java.util.concurrent.locks, они не добавляют ничего полезного в этом случае (хотя в других случаях они очень полезны). Мы всегда хотим подождать, пока данные не будут доступны, поэтому стандартный синхронизированный блок даст нам именно то, что нам нужно.

Ответ 2

Я думаю, что TimB очень хорошо объяснил большинство вариантов, но я думаю, что самый быстрый и самый очевидный ответ - создать его, когда экземпляр экземпляра будет создан.

class SomeClass {
    private final Map<String, String> someMap = new HashMap<String, String>();

    public String getValue(String key) {
        return someMap.get(key);  // the key might not exist even after initialization
    }
}

Ответ 3

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

  • Генерация/хранение каждого значения одинаково дорого
  • Генерация значений дорогая, но если вы ее генерируете, генерация остатка уже не так дорого (например, вам нужно запросить базу данных)

библиотека Guava имеет решение для обоих. Используйте Cache для генерации значений на лету или CacheLoader + loadAll для массового генерирования значений. Поскольку инициализация пустого кеша практически бесплатна, нет необходимости использовать двойную проверку idiom: просто присвойте экземпляр Cache private final поле.

Ответ 4

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

Карта aSynchronizedMap = Collections.synchronizedMap(новый HashMap());

class SomeClass {
    private Map<String, String> someMap = null;

    public String getValue(String key) {
        if (someMap == null) {
            synchronized (SomeClass.class) {

                  someMap  = Collections.synchronizedMap(new HashMap<String, String>());
                // initialize the map contents by loading some data from the database.
                // possible for the map to be empty after this.
            }
        }
        return someMap.get(key);  // the key might not exist even after initialization
    }
}