Как гарантировать, что get () ConcurrentHashMap всегда возвращает последнее фактическое значение?


введение
Предположим, у меня есть ConcurrentHashMap singleton:

public class RecordsMapSingleton {

    private static final ConcurrentHashMap<String,Record> payments = new ConcurrentHashMap<>();

    public static ConcurrentHashMap<String, Record> getInstance() {
        return payments;
    }

}

Тогда у меня есть три последующих запроса (все обработанные разными потоками) из разных источников.
Первый сервис делает запрос, который получает синглтон, создает экземпляр Record, генерирует уникальный идентификатор и помещает его в Map, затем отправляет этот идентификатор другому сервису.
Затем Вторая служба делает другой запрос с этим идентификатором. Он получает одноэлементный, находит экземпляр Record и изменяет его.
Наконец (вероятно, через полчаса) Вторая служба делает еще один запрос, чтобы изменить Record дальше.

проблема
В некоторых действительно редких случаях я испытываюheisenbug . В логах я вижу, что первый запрос успешно поместил Record в Map, второй запрос нашел его по ID и модифицировал, а затем третий запрос попытался найти запись по ID, но ничего не нашел (get() вернулся null).
Единственное, что я нашел о гарантиях ConcurrentHashMap, это:

Действия в потоке перед помещением объекта в любой параллельный поток сбор происходит-перед действиями, последующими за доступом или удалением этого элемента из коллекции в другом потоке.

Из здесь. Если я правильно понял, это буквально означает, что get() может вернуть любое значение, которое действительно было когда-то в карте, поскольку это не разрушает happens-before отношения между действия в разных потоках.
В моем случае это применимо следующим образом: если третьему запросу все равно, что произошло во время обработки первого и второго, то он может прочитать null из Map.

Это меня не устраивает, потому что мне действительно нужно получить от Map последнюю актуальную Record.

что я уже пробовал
Поэтому я начал думать, как сформировать отношение между последующими модификациями, и пришел с идеей. JLS говорит (в 17.4.4) это:

Запись в переменную volatile v (§8.3.1.4) синхронизирует-со всеми последующие чтения в любой ветке (где "после" определяется в соответствии с порядком синхронизации).

Итак, предположим, я изменю свой синглтон следующим образом:

public class RecordsMapSingleton {

    private static final ConcurrentHashMap<String,Record> payments = new ConcurrentHashMap<>();
    private static volatile long revision = 0;

    public static ConcurrentHashMap<String, Record> getInstance() {
        return payments;
    }

    public static void incrementRevision() {
        revision++;
    }
    public static long getRevision() {
        return revision;
    }

}

Затем, после каждой модификации Map или Record внутри, я вызову incrementRevision() и перед любым чтением с карты я вызову getRevision().

вопрос
Из-за природы гейзенбагс никакое количество тестов не достаточно, чтобы сказать, что это решение является правильным. И я не эксперт в параллелизме, поэтому не мог проверить это формально.

Может ли кто-то одобрить, что следование этому подходу гарантирует, что я всегда буду получать последнее фактическое значение от ConcurrentHashMap? Если этот подход неверен или кажется неэффективным, не могли бы вы порекомендовать мне что-то еще?

1 10

1 ответ:

Ваш подход не будет работать, так как вы на самом деле повторяете ту же ошибку снова. Поскольку ConcurrentHashMap.put и ConcurrentHashMap.get создадут отношение до, но без гарантии временного упорядочения, точно то же самое относится к вашим чтениям и записям в переменную volatile. Они формируютпроисходит до отношения, но нет гарантии упорядочения времени, если один поток вызовет get до того, как другой выполнит put, то же самое относится к volatile чтению, которое произойдет до volatile записи затем. Кроме того, вы добавляете еще одну ошибку, поскольку применение оператора ++ к переменной volatile не является атомарным.

Гарантии, сделанные для переменных volatile, не сильнее, чем гарантии, сделанные для переменных ConcurrentHashMap. это документация явно заявляет:

Извлечения отражают результаты самых последнихзавершенных операций обновления, выполненных после их начала.

Государств ПСБ, что внешние действия Интер-нить действия относительно порядка программы :

Действие между потоками-это действие, выполняемое одним потоком, которое может быть обнаружено или непосредственно затронуто другим потоком. Существует несколько видов действий между потоками, которые может выполнять программа:

...

  • Внешние Действия . Внешнее действие-это действие, которое может быть наблюдаемо вне выполнения и имеет результат, основанный на внешней среде выполнения.
Проще говоря, если один поток помещает в ConcurrentHashMap и отправляет сообщение внешнему объекту, а второй поток получает от того же самого ConcurrentHashMap после получения сообщения от внешнего объекта в зависимости от ранее отправленного сообщения, не может быть проблемы видимости памяти.

Это может быть случай, когда эти действия не запрограммированы таким образом или что внешняя сущность не имеет предполагаемой зависимости, но это может быть случай, когда ошибка лежит в совершенно другая область, но мы не можем сказать, так как вы не разместили соответствующий код, например, ключ не совпадает или код печати неправильный. Но что бы это ни было, оно не будет зафиксировано переменной volatile.