Java ConcurrentHashMap.computeIfНазначение изменения видимости

Скажем, у меня есть параллельная карта с коллекциями как ценность:

Map<Integer, List<Integer> map = new ConcurrentHashMap<>();
map.putIfAbsent(8, new ArrayList<>());

и я обновляю значение следующим образом:

map.computeIfPresent(8, (i, c) -> {
    c.add(5);
    return c;
});

Я знаю, что computeIfPresent вызов всего метода выполняется атомарно. Однако, учитывая, что к этой карте одновременно обращаются несколько потоков, я немного обеспокоен видимостью данных об изменениях, сделанных для базовой коллекции. В этом случае значение 5 будет отображаться в списке после вызова map.get

Мой вопрос будет изменен на список, который будет отображаться в других потоках при вызове map.get если изменения выполняются в computeIfPresent метода computeIfPresent.

Обратите внимание, что я знаю, что изменения в списке не будут видны, если я буду ссылаться на список перед выполнением операции обновления. Я не уверен, что изменения в списке будут видны, если я буду ссылаться на список (вызывая map.get) после операции обновления.

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

Более формально операция обновления для заданного ключа несет связь между событиями перед любым (ненулевым) извлечением для этого ключа, сообщающего обновленное значение

Ответы

Ответ 1

Чтобы уточнить ваш вопрос:

Вы предоставляете некоторую внешнюю гарантию, так что Map.computeIfPresent() вызывается перед Map.get().

Вы не указали, как вы это делаете, но позвольте сказать, что вы делаете это, используя что-то с помощью - перед семантикой, предоставляемой JVM. Если это так, то только это гарантирует, что List.add() виден в потоке, вызывающем Map.get() просто путем ассоциации отношений между Map.get().

Теперь, чтобы ответить на вопрос, который вы на самом деле спрашиваете: как вы уже отмечали, происходит связь между операцией обновления ConcurrentHashMap.computeIfPresent() и последующим вызовом метода доступа ConcurrentMap.get(). И, естественно, существует связь между List.add() и концом ConcurrentHashMap.computeIfPresent().

Отвечайте, да.

Там есть гарантия, что другой поток увидит 5 в List полученном через Map.get(), если вы гарантируете, что Map.get() на самом деле computeIfPresent() после computeIfPresent() (как указано в вопросе). Если последняя гарантия не работает, и Map.get() вызывается так, как только computeIfPresent() заканчивается, нет никаких гарантий того, что увидит другой поток, поскольку ArrayList не является потокобезопасным.

Ответ 2

Тот факт, что этот метод документирован как atomic, не означает значимости visibility (если это не является частью документации). Например, чтобы сделать это проще:

// some shared data
private List<Integer> list = new ArrayList<>();

public synchronized void addToList(List<Integer> x){
     list.addAll(x);
}

public /* no synchronized */ List<Integer> getList(){
     return list;
}

Мы можем сказать, что addToList действительно атомный, только один поток за раз может его назвать. Но как только определенный поток вызывает getList, просто нет гарантии о visibility (потому что для того, чтобы это было установлено, оно должно происходить при одной и той же блокировке). Таким образом, видимость - это нечто, что происходит раньше, и computeIfPresent документация вообще ничего не говорит об этом.

Вместо этого в документации по классу написано:

Операции поиска (включая get) обычно не блокируются, поэтому могут перекрываться с операциями обновления (включая put и remove).

Ключевым моментом здесь является, очевидно, перекрытие, поэтому некоторые другие потоки, вызывающие get (таким образом, удерживая этот List), могут видеть этот List в некотором состоянии; не обязательно состояние, в котором computeIfPresent (до того, как вы действительно вызвали get). Не забудьте прочитать дальше, чтобы понять, что это может на самом деле означать.

И теперь к самой сложной части этой документации:

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

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

Подумайте об этом, happens-before между двумя последующими действиями (например, в приведенном выше примере синхронизации); поэтому при обновлении Key может возникнуть волатильная письменная сигнализация о завершении обновления (я уверен, что это не так, просто пример). Для того, чтобы это произошло раньше, чтобы на самом деле работать, get должен прочитать, что volatile и увидеть состояние, которое было написано на него; если он видит это состояние, это означает, что до этого было установлено; и я предполагаю, что каким-то другим способом это фактически соблюдается.

Поэтому, чтобы ответить на ваш вопрос, во всех потоках, вызывающих get вы увидите last completed action которое произошло на этом ключе; в вашем случае, если вы можете гарантировать этот заказ, я бы сказал, да, они будут видны.

Ответ 3

c.add(5) не является потокобезопасным, внутреннее состояние c не защищено картой.

Точный способ создания отдельных значений и вставки-использования-удаления комбинаций потокобезопасности и состояния гонки зависит от шаблона использования (синхронизированная обертка, копирование на запись, блокировка свободной очереди и т.д.).