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