ConcurrentHashMap putIfAbsent : атомарность, когда следует вызов get()

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

Если бы я использовал ConcurrentHashMap, я мог бы сделать фамильяр

private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<K, V>();

public V getExampleOne(K key) {
    map.putIfAbsent(key, new Object());
    return map.get(key);
}

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

Чего бы я действительно хотел, так это чтобы все это было атомарным. Так,

public V getExampleTwo(K key) {
    return map.putIfAbsent(key, new Object());
}

но поскольку это расширяется до

if (!map.containsKey(key))
   return map.put(key, value);     [1]
return map.get(key);

который для строки [1] вернет null при первом использовании (т. е. map.put вернет предыдущее значение, которое при первом использовании равно null).

В этом случае я не могу вернуть null

Что оставляет меня с чем-то вроде;

public V getExampleThree(K key) {
    Object object = new Object();
    V value = locks.putIfAbsent(key, object);
    if (value == null)
        return object;
    return value;
}

Итак, наконец, мой вопрос; чем примеры выше отличаются по семантике?. Обеспечивает ли getExampleThree атомарность, аналогичную getExampleTwo, но правильно избегает возврата null? Есть ли другие проблемы с getExampleThree?

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

Я предполагаю, что это часть того, почему вы выбрали ConcurrentHashMap; что он виден/актуален/точен в тот момент, когда вы взаимодействуете с ним, но может быть влияние в дальнейшем, если старые данные станут проблемой...


person Toby    schedule 10.01.2012    source источник
comment
Вы разрешаете нули в качестве значений? Если да, то тот факт, что значение в getExampleThree равно null, не обязательно означает, что с ключом ничего не связано.   -  person Jirka    schedule 10.01.2012
comment
Более того, если вы возражаете против условий гонок в примере 1, в примере 3 есть условия гонок (когда ваш метод возвращает значение/объект, его может уже не быть на карте).   -  person Jirka    schedule 10.01.2012
comment
Значения Null никогда не добавляются, просто в getExample2 может быть возвращено значение null.   -  person Toby    schedule 10.01.2012


Ответы (4)


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

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

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


Если это не нормально, вы можете зациклить его.

public V getExampleOne(K key) {
    for(Object o = null, ret = null; (ret = map.get(key)) == null; )
        map.putIfAbsent(key, o == null ? o = new Object() : o);
    return ret;
}

его все еще можно удалить или заменить, как только петля существует, так что это практически то же самое, что и .

public V getExampleThree(K key) {
    Object o = new Object();
    map.putIfAbsent(key, o);
    Object ret = map.get(key);
    return ret == null ? o : ret;
}

Итак, наконец, мой вопрос; чем примеры выше отличаются по семантике?.

Разница только очевидна.

Обеспечивает ли getExampleThree атомарность, как и getExampleTwo, но правильно ли избегает нулевого возврата?

Да.

Есть ли другие проблемы с getExampleThree?

Только если вы считаете, что следующий вызов может не дать вам другое значение (если вы считаете, что его можно удалить в другом потоке).

person Peter Lawrey    schedule 10.01.2012
comment
Спасибо, его действительно можно использовать как глобальную сумку замков! - person Toby; 12.01.2012
comment
@JohnVint немного перегружен. ;) - person Peter Lawrey; 13.01.2012
comment
Просто перечитайте это, посмотрев на аналогичную проблему, отличный ответ. Большое спасибо - person Toby; 14.12.2012

Методы имеют разную семантику:

  • getExampleOne не является атомарным.
  • getExampleTwo возвращает null, если новый объект был вставлен на карту. Это отличается от поведения getExampleOne, но является атомарным.
  • getExampleThree, вероятно, то, что вы хотите. Он атомарный и возвращает объект, который находится на карте после момента времени вызова putIfAbsent. Но это было проблемой, когда нули являются допустимыми значениями в вашем приложении. Тогда возвращаемое значение null будет неоднозначным.

Однако в зависимости от ситуации это может быть не фактический объект в тот момент времени, когда вы используете возвращаемое значение. Затем вам нужна явная блокировка.

person dmeister    schedule 10.01.2012

Почему бы просто не использовать первую версию и не синхронизировать метод?

public synchronized V getExampleOne(K key) {
    map.putIfAbsent(key, new Object());
    return map.get(key);
}

Хотя это не обеспечит вам максимального параллелизма, верно и то, что у вас есть только две операции, и getExampleThree, хотя и правильно, менее удобочитаемо и менее понятно для кого-то другого, читающего ваш код.

person Tudor    schedule 10.01.2012

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

Мне не совсем понятно, что вы ищете. Дайте мне знать, если это не так, и я изменю.

Возможно, вы ищете что-то вроде:

private final ConcurrentHashMap<String, Object> map = new ConcurrentHashMap();

/*
 * Guaranteed to return the object replaced.
 * 
 * Note that by the time this method returns another thread 
 * may have already replaced the object again.
 * 
 * All we can guarantee here is that the return value WAS 
 * associated with the key in the map at the time the entry was 
 * replaced.
 * 
 * A null return implies that either a null object was associated
 * with the specified key or there was no entry in the map for 
 * the specified key wih 'null' as it's 'value'.
 */
public Object consistentReplace ( String key, Object newValue ) {
  Object oldValue = map.get(key);
  while ( !map.replace(key, oldValue, newValue) ) {
    // Failed to replace because someone got in there before me.
    oldValue = map.get(key);
  }
  return oldValue;
}
person OldCurmudgeon    schedule 12.01.2012