Безопасная публикация и ConcurrentHashMap - PullRequest
0 голосов
/ 29 марта 2020

Давайте предположим, что у нас есть класс Container

class Container {
      private final Map<LongHolder, Integer> map = new ConcurrentHashMap<>();
      static class LongHolder {
         private final Long i;
         private LongHolder(Long i) {
            this.i = i;
         }
      }

      public LongHolder createValue(Long i) {
          LongHolder v = new LongHolder(i);
          map.put(v, 1)
          return LongHolder;
      }

      public void doSmth(LongHolder longHolder) {
         map.get(longHolder);
         ... // do smth
      }
}

Можно ли переупорядочить операции таким образом, что ссылка на LongHolder из createValue избегает метода и, таким образом, становится доступной для других потоков перед тем, как поместить его в карту ? В этом случае мы можем получить форму ссылки LongHolder createValue и передать ее в doSmth в другом потоке, который не увидит его на карте. Является ли это возможным? Если нет, пожалуйста, объясните.

Обновление: Javado c из ConcurrentHashMap сообщает Извлечения отражают результаты самых последних выполненных операций обновления, проведенных с момента их появления. (Более формально, операция обновления для данного ключа имеет отношение «происходит до» с любым (не нулевым) поиском для этого ключа, сообщающего об обновленном значении.) Но есть из последних завершенных операций обновления вопрос был как раз об этом тонком случае, когда переупорядочение потенциально могло произойти так, что на самом деле операция обновления не была завершена, и ссылка была сброшена до put. Javado c из ConcurrentHashMap констатирует только тот факт, что данный ключ имеет отношение «происходит до» с любым (не нулевым) поиском для этого ключа другими словами, только если мы получим поиск по этому ключу, мы можем обсудить вопрос о до связан с операцией обновления для этого ключа.

Я могу только предложить несколько других причин, почему этого не может произойти:

1) что правила переупорядочения являются более строгими, чем я думал, и мы не можем допустить произвольного изменения порядка (или можем? и каковы правила?).

2), что у put есть некоторые маги c pro perties, которые не позволяют повторный вызов и return происходит только после завершения put. Что это за свойства в этом случае?

1 Ответ

2 голосов
/ 29 марта 2020

Предположим, что потоки A и B имеют ссылку на экземпляр Container.

(Поскольку Container.map объявлен как final, его значение будет безопасно опубликовано. Кроме того, , поскольку он ссылается на ConcurrentHashMap, синхронизация не требуется для обеспечения безопасности потока.)

Теперь предположим, что поток A вызывает Longholder holder = container.createValue(x) и каким-то образом передает holder потоку B. Ваш вопрос: если B звонит doSmth, проходя мимо этого держателя, увидит ли map.get(holder) звонок его на карте?

Ответ - Да, будет.

Спецификация для ConcurrentHashMap это говорит:

"Извлечения отражают результаты самых последних завершенных операций обновления, выполненных с момента их появления. (Более формально, операция обновления для данного ключа переносится -перед связью с любым (не нулевым) поиском для этого ключа, сообщающего об обновленном значении.) ".

Это означает, что происходит до из put вызовите этот поток A и последующий get вызов в поток B. Это, в свою очередь, означает, что:

  • get найдет клавишу LongHolder, а
  • get верните правильное значение.

Значение поля LongHolder объекта *1052* также будет соответствовать ожидаемому, и даже если LongHolder было бы изменяемым holder.

(Обратите внимание, что нет смысла объявлять неизменным LongHolder здесь. Это эквивалентно java.lang.Long ... хотя и с более простым API.)


Можно ли переупорядочить операции таким образом, чтобы ссылка на LongHolder из createValue экранировала метод и, таким образом, стала доступна для других потоков перед тем, как поместить его в карту?

В основном нет. Соответствующие операции таковы:

      LongHolder v = new LongHolder(i);
      map.put(v, 1)
      return v;    // corrected typo
  1. Нет никакого изменения порядка исходного кода, который имеет какой-либо смысл.
  2. Если вы говорите о переупорядочении компилятором относительно потока, который работает createValue, JLS не разрешает это. Любое изменение порядка, которое изменяет видимость внутри потока, запрещено.
  3. Публикация v через карту является безопасной публикацией благодаря свойствам get и put. (При реализации существуют барьеры памяти, которые запрещают вредные изменения порядка вызовов get и put.)

  4. и 3. являются следствием происходит до отношений.

Теперь, если код должен был измениться на это:

      LongHolder v = new LongHolder(i);
      map.put(v, 1)
      // Somehow modify v.i
      return v;

затем , что будет формой небезопасной публикации. Поскольку изменение v.i на A происходит после put, происходит до того, как соотношение между put и get в B недостаточно для того, чтобы гарантировать новое значение v.i видно в потоке B. Это потому, что цепочка больше не работает.

Теперь я предполагаю, что если результат вызова createValue в потоке A был передан другому потоку (B или C) небезопасным способом, тогда последнему не гарантируется правильное значение для v.i ..., если LongHolder является изменяемым. Но это не проблема с кодом createValue / doSmth. И публикация значения v через карту безопасна.

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

...