Атомарные обновления значений в параллельной хэш-карте - как?

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

ConcurrentHashMap<String, ProcessMetaData> RUNNING_PROCESSES = new ConcurrentHashMap();

С безопасным размещением новых объектов на карте все в порядке, проблема в том, что состояние этих процессов меняется, поэтому мне приходится время от времени обновлять ProcessMetaData. Я сделал ProcessMetaData неизменяемым и использовал метод compute() ConcurrentHashMap для обновления значений, но теперь проблема в том, что ProcessMetaData усложняется, и сохранение неизменности становится трудно управляемым. Вопрос в том, что пока я обновляю только ProcessMetaData в атомарном (согласно javadoc) методе compute(), объект может быть изменчивым, и в целом все по-прежнему будет потокобезопасным? Верно ли мое предположение?


person Yuri Dolzhenko    schedule 01.06.2021    source источник
comment
Если предположить, что читатель ProcessMetaData использует вычисления, тогда да. В противном случае читатели увидят расы, поскольку они изменяются под ними (например, эквивалентно использованию synchronized (processMetaData) { ... }). Неизменяемый экземпляр означает, что читателям не нужно работать под монопольной блокировкой, поэтому, если вы удалите ее, вам нужно координировать свои действия.   -  person Ben Manes    schedule 02.06.2021
comment
Да, теперь это имеет смысл, большое спасибо.   -  person Yuri Dolzhenko    schedule 02.06.2021
comment
@BenManes подразумевает ли атомарность видимость? Я имею в виду тот факт, что compute задокументировано как atomic, означает, что он также предоставляет visibility? Теоретически реализация может блокировать разные блокировки для чтения и записи (придуманный пример), поэтому в таком случае читатели не гарантируют, что увидят обновления.   -  person Eugene    schedule 02.06.2021
comment
@Eugene на аппаратном уровне концепция представляет собой барьер памяти для обеспечения упорядочения памяти. Читателю нужен барьер загрузки, чтобы увидеть изменения (или он может никогда их не увидеть), а писателю нужен барьер хранения, чтобы их опубликовать (иначе они могут никогда не стать доступными для чтения). Атомарность заключается в том, что все изменения могут быть видны или нет для защищенных данных (нет частичная запись). В противном случае читатели могут увидеть только некоторые изменения и вызвать недетерминированное поведение. Разрешение гонок данных должно быть предусмотрено.   -  person Ben Manes    schedule 02.06.2021
comment
@BenManes спасибо, я понял. Просто compute не задокументирован ни с какими гарантиями до того, как произойдет, как и с классом. Единственная гарантия исходит от пакета java.util.concurrent с: Действия в потоке до помещения объекта в любую параллельную коллекцию происходят до действий, следующих за доступом или удалением этого элемента из коллекции в другом потоке, но это все еще не достаточно сказать, что compute предлагает необходимые гарантии в отношении happens-before.   -  person Eugene    schedule 02.06.2021
comment
@Eugene Eugene Класс javadoc действительно обсуждает, что происходит раньше. Если у вас есть предложения, отправьте электронное письмо по адресу concurrency-interest, так как Дуг очень приветствует изменения, добавляющие ясности.   -  person Ben Manes    schedule 02.06.2021
comment
Если вычисление обновляет экземпляр ProcessMetaData, происходит гонка данных. Если вычисление создает новый экземпляр ProcessMetaData на основе существующего (поэтому он фактически неизменяем), то гонки данных быть не должно.   -  person pveentjer    schedule 03.06.2021
comment
@Eugene, читающий из ConcurrentHashMap, вообще не использует блокировку. Да, это отличается от того, что делает compute, и делает операцию compute, которая изменяет уже сохраненный объект неатомарным в отношении операций извлечения. Но даже если вы используете только compute, вам не разрешается использовать возвращаемое значение, поскольку это не будет частью атомарной операции.   -  person Holger    schedule 03.06.2021
comment
@Holger Хольгер, да, у нас уже были эти дебаты. Использование get с compute небезопасно, поэтому get говорит, что это может перекрываться. Я понимаю. Вопрос, который у меня был, был compute для обновлений, compute для поиска, как Бен предложил ранее.   -  person Eugene    schedule 03.06.2021
comment
@Eugene Я не вижу слова «извлечение» в предыдущем обсуждении. функция обязательно воспримет предыдущее состояние. Использование compute для извлечения — это другая проблема, так как, конечно, он будет воспринимать завершенное состояние обновления, которое он только что выполнил в том же потоке. Проблемы начинаются, как объяснено в моем ответе, с другими операциями compute при использовании полученного значения. Нет упорядочения и видимости памяти (как это могло быть?) при использовании извлеченного значения, в то время как другие compute операции изменяют его.   -  person Holger    schedule 03.06.2021
comment
@pveentjer после двух дней размышлений над этим вопросом, я думаю, ваш комментарий резюмирует, что должен делать ОП. Это отличная идея, жаль, что вы редко переводите свои комментарии в ответы.   -  person Eugene    schedule 05.06.2021


Ответы (1)


Пока вы получаете доступ только к значению внутри функции, переданной compute, изменения, сделанные в этой функции, безопасны.

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

Метод compute возвращает значение результата точно так же, как get возвращает текущее сохраненное значение. Как только вызывающий объект начинает использовать это значение, это использование может быть параллельным последующим compute операциям на карте. Метод get может получить значение даже во время выполнения операции compute. Разрешение неблокирующих операций извлечения — одна из основных функций ConcurrentHashMap. Таким образом, могут возникнуть все виды условий гонки.

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

person Holger    schedule 03.06.2021
comment
мне потребовалось всего около 2 дней, чтобы понять, что это правильно. - person Eugene; 08.06.2021