Вызывает ли повторное помещение объекта в ConcurrentHashMap отношение памяти «происходит раньше»? - PullRequest
8 голосов
/ 18 октября 2011

Я работаю с существующим кодом, который имеет хранилище объектов в форме ConcurrentHashMap. Внутри карты хранятся изменяемые объекты, которые используются несколькими потоками. Никакие два потока не пытаются изменить объект сразу по проекту. Мое беспокойство касается видимости изменений между потоками.

В настоящее время код объектов синхронизируется с «сеттерами» (охраняется самим объектом). На «получателях» нет синхронизации, и члены не изменчивы. Для меня это будет означать, что видимость не гарантируется. Однако, когда объект модифицируется, он повторно помещает обратно в карту (метод put() вызывается снова, тот же ключ). Означает ли это, что когда другой поток вытаскивает объект из карты, он видит изменения?

Я исследовал это здесь на stackoverflow, в JCIP и в описании пакета для java.util.concurrent. Я в основном запутался, я думаю ... но последняя капля, которая заставила меня задать этот вопрос, была из описания пакета, оно гласит:

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

Что касается моего вопроса, включают ли "действия" модификации объектов, сохраненных на карте перед повторной установкой ()? Если все это приводит к видимости между потоками, является ли это эффективным подходом? Я относительно новичок в темах и буду признателен за ваши комментарии.

Edit:

Спасибо всем за ответы! Это был мой первый вопрос о StackOverflow, и он мне очень помог.

Я должен идти с ответом ptomli , потому что я думаю, что это наиболее ясно решило мою путаницу. То есть установление отношения «происходит до» не обязательно влияет на видимость модификации в этом случае. Мой «заглавный вопрос» плохо составлен относительно моего фактического вопроса, описанного в тексте. Ответ ptomli теперь совпадает с тем, что я прочитал в JCIP : «Чтобы все потоки видели самые последние значения общих изменяемых переменных, потоки чтения и записи должны синхронизировать по общей блокировке »(стр. 37). Повторное помещение объекта обратно на карту не обеспечивает эту общую блокировку для изменения элементов вставленного объекта.

Я ценю все советы по изменению (неизменяемые объекты и т. Д.), И я полностью согласен с этим. Но для этого случая, как я уже упоминал, нет параллельной модификации из-за тщательной обработки потоков. Один поток изменяет объект, а другой поток позже читает объект (при этом CHM является конвейером объекта). Я думаю, что CHM недостаточно, чтобы гарантировать, что последующий выполняющий поток увидит изменения от первого, учитывая ситуацию, которую я предоставил. Тем не менее, я думаю, что многие из вас правильно ответили на заглавный вопрос .

Ответы [ 6 ]

7 голосов
/ 18 октября 2011

Вы вызываете concurrHashMap.put после каждой записи в объект. Однако вы не указали, что также вызываете concurrHashMap.get перед каждым чтением. Это необходимо.

Это верно для всех форм синхронизации: вам нужно иметь несколько «контрольных точек» в обоих потоках. Синхронизация только одного потока бесполезна.

Я не проверял исходный код ConcurrentHashMap, чтобы убедиться, что put и get запускают событие раньше, но вполне логично, что они должны.

Однако в вашем методе все еще есть проблема, даже если вы используете put и get. Проблема возникает, когда вы изменяете объект, и он используется (в несогласованном состоянии) другим потоком до того, как он станет put. Это тонкая проблема, потому что вы можете подумать, что старое значение будет прочитано, поскольку оно еще не было put, и это не вызовет проблемы. Проблема в том, что когда вы не синхронизируете, вы не гарантированно получите согласованный более старый объект, а поведение не определено. JVM может обновлять любую часть объекта в других потоках в любое время. Только при использовании какой-либо явной синхронизации вы уверены, что обновляете значения согласованным образом между потоками.

Что вы могли бы сделать:
(1) синхронизировать все обращения (геттеры и сеттеры) к вашим объектам повсюду в коде. Будьте осторожны с установщиками: убедитесь, что вы не можете установить объект в несовместимое состояние. Например, при установке имени и фамилии наличие двух синхронизированных сеттеров недостаточно: необходимо установить блокировку объекта для обеих операций вместе.
или
(2) когда вы put объект на карте, поместите глубокую копию вместо самого объекта. Таким образом, другие потоки никогда не будут читать объект в несогласованном состоянии.

EDIT :
Я только что заметил

В настоящее время код объектов имеет синхронизацию с «сеттерами» (охраняется самим объектом). Там нет синхронизации на «добытчики» и члены не изменчивы.

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

5 голосов
/ 18 октября 2011

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

Если ваш код идет

  • CHM # get
  • вызовите различные сеттеры
  • CHM # put

тогда «случайное до», предоставляемое путом, гарантирует, что все вызовы mutate будут выполнены перед путом. Это означает, что любое последующее получение будет гарантированно видеть эти изменения.

Ваша проблема в том, что фактическое состояние объекта не будет детерминированным, потому что, если фактический поток событий равен

  • Тема 1: CHM # get
  • поток 1: установщик вызовов
  • поток 2: CHM # get
  • поток 1: установщик вызовов
  • поток 1: установщик вызовов
  • Тема 1: CHM # положить

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

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

3 голосов
/ 18 октября 2011

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

Что происходит с получением, происходящим до повторного переноса, но покамодификации происходят.Они могут видеть только некоторые изменения, и объект будет иметь несовместимое состояние.

Если вы можете, я бы рекомендовал хранить неизменяемые объекты на карте.Затем любой get получит версию объекта, которая была текущей, когда он выполнял get.

3 голосов
/ 18 октября 2011

Я думаю, что ваш вопрос больше относится к объектам, которые вы храните на карте, и к тому, как они реагируют на одновременный доступ, чем к самой параллельной карте.

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

Вычтите Map из уравнения и определите, являются ли хранимые вами экземпляры потокбезопасны сами по себе.

Однако, когда объект модифицируется, он возвращается на карту (снова вызывается метод put (), тот же ключ).Означает ли это, что когда другой поток вытаскивает объект из карты, он видит изменения?

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

2 голосов
/ 18 октября 2011

Это фрагмент кода из java.util.concurrent.ConcurrentHashMap (Open JDK 7):

  919       public V get(Object key) {
  920           Segment<K,V> s; // manually integrate access methods to reduce overhead
  921           HashEntry<K,V>[] tab;
  922           int h = hash(key.hashCode());
  923           long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
  924           if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
  925               (tab = s.table) != null) {
  926               for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
  927                        (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
  928                    e != null; e = e.next) {
  929                   K k;
  930                   if ((k = e.key) == key || (e.hash == h && key.equals(k)))
  931                       return e.value;
  932               }
  933           }
  934           return null;
  935       }

UNSAFE.getObjectVolatile() является задокументированным как получатель с внутренним volatileсемантика, таким образом, барьер памяти будет пересекаться при получении ссылки.

1 голос
/ 18 октября 2011

да, put подвергается энергозависимой записи, даже если ключ-значение уже существует на карте.

Использование ConcurrentHashMap для публикации объектов в потоке довольно эффективно. Объекты не должны быть изменены дальше, когда они находятся на карте. (Они не должны быть строго неизменными (с полями в конце))

...