Является ли этот ленивый шаблон инициализации для объектов в хэш-карте потокобезопасным? - PullRequest
2 голосов
/ 08 сентября 2011

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

Это хорошая конструкция?

private final Map<String, Stuff> stash = new HashMap<String, Stuff>();

public Stuff getStuff(String name) {

    if (stash.containsKey(name))
        return stash.get(name);

    synchronized(stash) {
        if (stash.containsKey(name)) {
            return stash.get(name);
        }
        else {
            Stuff stuff = StuffFactory.create(name);
            stash.put(name, stuff);
            return stuff;
        }
    }
}

Ответы [ 2 ]

6 голосов
/ 08 сентября 2011

Нет, эта конструкция не является поточно-ориентированной.

Предположим, что нить writer помещает что-то в карту, а размер карты слишком мал, поэтому ее размер нужно изменить.Это делается внутри блока synchronized, поэтому вы можете думать, что у вас все хорошо.

Во время изменения размера ничто на карте не гарантируется.

Теперь, в то же самое время, предположим,что поток reader вызывает getStuff для существующего элемента.Этот поток может получить прямой доступ к карте, поскольку он не попадает в блок synchronized при первом вызове containsKey и get.Он найдет карту в неопределенном состоянии и, хотя он только читает, он получает доступ к данным, содержимое которых не определено.Среди возможных результатов:

  • getStuff возвращает null, когда это не должно быть.
  • getStuff возвращает ожидаемое Stuff.
  • getStuff возвращает некоторый внутренний объект, который используется реализацией HashMap во время изменения размера.
  • getStuff возвращает некоторый другой Stuff, не связанный с именем.
  • getStuff попадаетв бесконечном цикле.

Это просто очевидный случай, который должен быть прост для понимания.Так что нет, не используйте ярлыки, когда есть хорошо разработанные классы, такие как ConcurrentHashMap или Guava's MapMaker.

Кстати: Звоните containsKey сначала, а потом get с тем же ключом довольно неэффективно.Просто позвоните get, сохраните результат и сравните его с null.Вы сохраните одну поисковую операцию на карте.

1 голос
/ 08 сентября 2011

Замените HashMap на ConcurrentHashMap, ваш вывод в порядке.

Более общие решения должны беспокоиться о следующих вещах

1.блокировка на основе имени, а не глобальная блокировка.если create(n1) блокируется, это не должно влиять на операции с другими именами.

2.Что делать, если create() возвращает ноль или выбрасывает исключение.Интересно, что это проблема для некоторых импл.

3.что если create(n1) позвонит get(n1)?у нас будет рекурсиянекоторые импл виснет.некоторые impl обнаруживает рекурсию и выдает ошибку.лучший impl должен позволять запускать рекурсию (которая либо завершается, либо переполняется стеком), а промежуточный результат должен быть видим для потока блокировки, но невидим для других потоков, пока рекурсия не будет завершена.

...