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

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

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, второй запрос нашел его по идентификатору и изменил его, а затем третий запрос попытался найти запись по идентификатору, но ничего не нашел (get() вернул null).
Единственная вещь, которую я нашел о ConcurrentHashMap гарантиях, это:

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

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

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

Что я пробовал
Итак, я начал думать, как сформировать happens-before отношения между последующими Map модификациями; и пришла с идеей. JLS говорит (в 17.4.4), что:

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

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

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().

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

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


person mkrakhin    schedule 21.04.2015    source источник
comment
Возможно, я неправильно прочитал ваш вопрос здесь, но вы, кажется, предполагаете, что чтение всегда будет происходить после записи; Почему?   -  person fge    schedule 21.04.2015
comment
@fge, хотя это независимые потоки. Это заранее заданная последовательность (положил-получил-получил), которая меня интересует и иногда идет не так.   -  person mkrakhin    schedule 21.04.2015
comment
Что ж, в любом случае это пойдет не так; вы не можете гарантировать порядок выполнения потоков, если не защитите их друг от друга, так что это выглядит почти как безнадежное дело; то, что вы называете последним значением, - это то значение, которое было записано последним записывающим потоком, поэтому до сих пор оно согласовано. Насколько я понимаю, вам нужен какой-то другой механизм синхронизации.   -  person fge    schedule 21.04.2015
comment
@fge ну, эта вторая служба отправляет мне второй запрос только после того, как я пришлю ему идентификатор. И отправляет третий запрос только после того, как я пришлю ему ответ на второй запрос. Я думаю, что эта последовательность вызвана логикой потока. Вторая служба не может отправить второй запрос, пока не получит от меня запрос, содержащий идентификатор, и не может отправить третий запрос, пока не получит ответ на второй.   -  person mkrakhin    schedule 21.04.2015
comment
Я предполагаю в этой последовательности T1: put(a); put(b), T2: get(b); get(a). Если get(b) вернет ожидаемое значение, get(a) также всегда вернет ожидаемое значение. нет?   -  person Kelvin Ng    schedule 21.04.2015
comment
Все ли ваши потоки находятся в одном ClassLoader?   -  person Rich    schedule 21.04.2015
comment
@KelvinNg Я тоже так полагаю (в случае put(b) изменения какого-то изменчивого поля), но не уверен. И меня интересуют случаи, когда я не знаю ожидаемого значения b.   -  person mkrakhin    schedule 21.04.2015
comment
@Rich в моем случае это все потоки из пула, который сервлет-контейнер использует для обработки запросов. Так что я полагаю, что да.   -  person mkrakhin    schedule 21.04.2015
comment
Вы уверены, что на каждом этапе используется один и тот же ключ? Например, мог ли быть введен непечатаемый символ, из-за которого карта не находит правильного значения? например, mykey vs mykey.   -  person Rich    schedule 21.04.2015
comment
@Rich да, вторая служба должна прислать мне ту же строку, которую я ей послал, и эта строка содержит только буквенно-цифровые символы (из-за метода ее генерации).   -  person mkrakhin    schedule 21.04.2015
comment
@mkrakhin Если один поток сначала помещает запись в карту, а затем уведомляет другие службы о необходимости извлечения с карты, не должно быть случая, когда элемент равен нулю. Это то, что вы видите? Или есть гонка между записью в карту и уведомлением другого сервиса о чтении карты?   -  person John Vint    schedule 21.04.2015
comment
Я подозреваю, что я не имею ничего общего с ConcurrentHashMap: это связано с доступом к Record. Все его поля должны быть непостоянными или доступ к ним синхронизирован. В противном случае они подпадают под правило «не произошло раньше».   -  person user207421    schedule 21.04.2015
comment
@EJP: ключ - String, а результат get() - null, поэтому нет ничего внутри типа Record, которое могло бы вызвать проблему.   -  person Holger    schedule 21.04.2015
comment
Разве помещение значения в ConcurrentHashMap не гарантирует безопасную публикацию этого значения?   -  person Rich    schedule 21.04.2015
comment
@JohnVint, это действительно то, что я вижу, здесь нет гонки (а также кода, удаляющего записи с карты). Например, однажды я увидел в журналах следующее: запись была помещена на карту, следующий запрос пришел через минуту и ​​успешно получил его, затем третий запрос пришел через полчаса и не нашел записи .   -  person mkrakhin    schedule 21.04.2015
comment
Не могли бы вы добавить в свои журналы ключ, хэш-код ключа, toString параллельной хэш-карты во время первоначального ввода и после получения, которое вернуло значение null, чтобы увидеть, действительно ли вы имеете дело с одним и тем же?   -  person Rich    schedule 21.04.2015
comment
@mkrakhin Я знаю, что вы не хотите этого слышать, но если вы буквально ждете полчаса, а запись не найдена, значит, в вашем коде что-то не так. Полчаса - это вечность для системы, и не будет всплывать гонка данных. Опять же, если это действительно полчаса, я бы устранил проблемы с памятью, которые случаются, прежде чем заказывать как возможность и искать в другом месте.   -  person John Vint    schedule 21.04.2015
comment
@Rich, если я правильно понимаю документацию, get() может вернуть любое значение, которое однажды хранилось под этим ключом; и затем он гарантирует, что любые действия, которые happened-before помещают полученное значение в map happened-before, получают это значение.   -  person mkrakhin    schedule 21.04.2015
comment
Еще одна возможность - запись не расширяет SoftReference, не так ли?   -  person Rich    schedule 21.04.2015
comment
@Rich нет, и не фантомная ссылка.   -  person mkrakhin    schedule 21.04.2015
comment
@Rich Я, наверное, мог бы попытаться предоставить журналы, но я не уверен, сколько времени у меня уйдет, чтобы испытать эту ошибку еще раз.   -  person mkrakhin    schedule 21.04.2015
comment
@JohnVint да, это определенно то, что я не хотел слышать: D Честно говоря, я уже потратил много времени, пытаясь понять, что не так, и возможные барьеры памяти были моей последней идеей :)   -  person mkrakhin    schedule 21.04.2015
comment
@mkrakhin Чтобы не сходить с ума, вы можете распечатать ссылку на объект синглтона. Размещайте вход после каждого put и get с идентификатором хэш-кода объекта. System.identityHashCode(this) Возвращаемое значение будет ссылкой на память. Я думаю, что это скорее что-то со ссылками, если не будет дополнительной мутации.   -  person John Vint    schedule 21.04.2015
comment
Кроме того, на скольких экземплярах вы это развернули? Есть ли какая-то балансировка нагрузки?   -  person John Vint    schedule 21.04.2015
comment
@JohnVint ConcurrentHashMap переопределяет hashCode(), поэтому это не ссылка на память, а хэш элементов внутри. В любом случае, я мог сравнить hashCode после записи и перед следующим чтением, но когда я попытался это сделать, все работало, как ожидалось, долгое время, и у меня не было никаких ошибок :(   -  person mkrakhin    schedule 21.04.2015
comment
Есть два типа хэш-кодов. Существует хэш-код, который генерирует объект (хэш-код содержимого), если он переопределяет. И есть System.identityHashCode, который всегда будет давать вам ссылку на объект в памяти. Используйте System.identityHashCode   -  person John Vint    schedule 21.04.2015
comment
@JohnVint хм, он развернут на Jelastic, без какого-либо балансировщика нагрузки и на одном узле (но для этого узла есть несколько вещей, называемых cloudlets, поэтому я не мог быть уверен, что физически это действительно один сервер).   -  person mkrakhin    schedule 21.04.2015
comment
@mkrakhin Если есть несколько экземпляров, которые каким-то образом сбалансированы по нагрузке, я уверен, что это ваша проблема. Получение ссылки на память синглтона может помочь в отладке и подтверждении этого.   -  person John Vint    schedule 21.04.2015
comment
@JohnVint О, я пробовал, действительно дает ссылку на память. Я неправильно истолковал JavaDoc для identityHashCode. Хорошо, я попробую, спасибо.   -  person mkrakhin    schedule 21.04.2015


Ответы (1)


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

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

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

JLS заявляет, что внешние действия - это межпоточные действия в отношении порядок программы:

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

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

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

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

person Holger    schedule 21.04.2015
comment
Ваша точка зрения интересна. Я думал, что модификатор volatile предлагает какие-то другие гарантии (вроде, если чтение действительно было после записи, он увидит правильное значение), чем методы ConcurrentHashMap (вы можете увидеть мои мысли по этому поводу в комментариях к вопросу ). Кстати, спасибо, что напомнили, что инкремент не атомарный, это моя невнимательность. - person mkrakhin; 21.04.2015
comment
Еще нужно добавить, что JavaDoc для ConcurrentHashMap запрещает вставку нуля в качестве ключа или значения - это доказывает, что ключ, который вы ищете, не существует на карте. - person Rich; 21.04.2015
comment
Что ж, если чтение было «действительно после записи», вы увидите правильное значение, но то же самое относится к get и put из ConcurrentMap. Обратите внимание, что если ваш код был упорядочен в соответствии с описанными вами намерениями, то есть что связь с внешним объектом подразумевает упорядочение, API, который вы используете для связи, действительно предоставит вам упорядочение, которое расширяется до операций ConcurrentMap. Но поскольку вы сталкиваетесь с проблемами, код, очевидно, не устроен таким образом (если настоящая ошибка не связана с печатью или несвязанной областью ...) - person Holger; 21.04.2015
comment
@Holger, как я уже говорил, общение на самом деле подразумевает порядок. Запросы безоговорочно последующие. Так что, похоже, проблема действительно в какой-то другой области. Не могли бы вы привести цитаты из спецификации, доказывающие, что ConcurrentHashMap действительно предоставляет эти гарантии, чтобы я мог принять ваш ответ? - person mkrakhin; 21.04.2015
comment
Что ж, Алексей Шипилев из Oracle подтвердил, что ConcurrentHashMap действительно должен возвращать самое последнее обновление при извлечении. Тогда я принимаю этот ответ. - person mkrakhin; 21.04.2015