Модель памяти Java на практике

Я пытался изучить модель памяти Java., но до сих пор не могу понять, как люди используют его на практике.

Я знаю, что многие просто полагаются на соответствующие барьеры памяти (как описано в поваренной книге), но ведь сама модель такими терминами не оперирует. Модель вводит различные заказы, определенные для набора действий, и определяет так называемые «правильно сформированные исполнения». Некоторые люди пытаются объяснить ограничения модели памяти, используя один из таких порядков, а именно «происходит-до», но похоже, что порядок, по крайней мере, сам по себе, не определяет приемлемое выполнение:

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

Мой вопрос: как можно проверить, что определенный код или изменение могут привести к «незаконному выполнению» на практике (согласно модели)?

Чтобы быть более конкретным, давайте рассмотрим очень простой пример:

public class SomeClass {
   private int a;
   private int b;

   public void someMethod() {
      a = 2; // 1 
      b = 3; // 2  
   }   
   // other methods
}

Понятно, что внутри потока w(a = 2) происходит раньше w(b = 3) согласно порядку программы. Как компилятор/оптимизатор может быть уверен, что изменение порядка 1 и 2 не приведет к «незаконному выполнению» (строго с точки зрения модели)? И почему, если мы установим b изменчивым, это произойдет?


person AngryJuice    schedule 03.07.2014    source источник


Ответы (2)


Вы спрашиваете о том, как VM/JIT анализирует поток байт-кода? Это слишком широко, чтобы ответить, об этом написаны целые исследовательские работы. И то, что на самом деле реализует виртуальная машина, может меняться от выпуска к выпуску.

Или вопрос просто в том, какие правила модели памяти управляют тем, что является «законным»? Для исполняемого потока модель памяти уже обеспечивает надежную гарантию того, что каждое действие в заданном потоке представляется происходящим в программном порядке для этого потока. Это означает, что если JIT определяет, какой метод(ы) он реализует для переупорядочивания, что переупорядочивание дает тот же самый наблюдаемый результат(ы), что является законным.

Наличие действий, устанавливающих гарантии "происходит до" в отношении других потоков (таких как доступ к volatile), просто добавляет больше ограничений к законному переупорядочению.

Упрощенно это можно запомнить как то, что все, что происходило в программном порядке до, также кажется (уже) произошедшим с другими потоками, когда выполняется действие «происходило-до-установки».

Для вашего примера это означает, что в случае энергонезависимого (a, b) необходимо поддерживать только гарантию «похоже, что это происходит в порядке программы» (для исполняемого потока), что означает любое изменение порядка записи в (a, б) является допустимым, даже задержка их до фактического чтения (например, сохранение значения в регистре ЦП и обход основной памяти) будет допустимой. Он может даже вообще не записывать члены, если JIT обнаружит, что они никогда не читаются до того, как объект выйдет из области видимости (и, если быть точным, их также не использует финализатор).

Если сделать b volatile в вашем примере, изменяются ограничения в том, что другие потоки, читающие b, также гарантированно увидят последнее обновление a, потому что это произошло до записи в b. Опять же упрощенно, действия «происходит до того, как» распространяют некоторые предполагаемые гарантии упорядочения от исполняемого потока к другим потокам.

person Durandal    schedule 03.07.2014
comment
Да, вопрос в основном в том, как можно определить и доказать, используя термины модели памяти, какое переупорядочивание является законным. Случается раньше, это не то, что полностью определяет это, насколько я понимаю: для этого конкретного случая в энергонезависимом варианте 1 и 2 могут быть переупорядочены и, по-видимому, происходит даже для исполняемого потока в не программном порядке, поскольку есть нет зависимости данных/управления, и это ничего не сломает. Это нарушает отношение HB, которое соответствует модели. Для volatile case это не так, поэтому некоторые нарушения разрешены, а некоторые нет. Вопрос в критериях. - person AngryJuice; 03.07.2014
comment
Я бы назвал действия, перечисленные в docs.oracle.com/javase/specs/jls/se8/html/ при синхронизации действий критерии, ограничивающие изменение порядка. Что касается энергонезависимого примера, записи не могут не казаться происходящими в обратном порядке (b, a) для потока - вы смешиваете физический порядок (порядок, в котором это происходит в ЦП) с логическим порядком (порядок, который программа может соблюдать). Грубо говоря, любая переделка, которая происходит за вашей спиной (без вашего ведома), является законной. - person Durandal; 03.07.2014
comment
С точки зрения JMM нет физических или логических ордеров, а также ЦП, кешей и т. д. Он определяет только действия и выполнения. И с этой точки зрения действие, которое записывает в B, и действие, которое записывает в A в одном и том же потоке, могут отображаться в непрограммном порядке, если результирующее выполнение правильно сформировано. Поправьте меня, если я ошибаюсь. - person AngryJuice; 03.07.2014
comment
Да, JMM явно говорит только о действиях и порядке выполнения (это то, что я назвал логическим порядком). Это намекает на то, что виртуальная машина может переупорядочивать действия, не называя исходную и переупорядоченную последовательности действий (переупорядоченная последовательность — это то, что я назвал физическим порядком). Я не понимаю, что вы хотите спросить/заявить с этой точки зрения... Никакое преобразование, которое заметно изменяет порядок программы, не является законным. Это основная суть вещей, которые, кажется, происходят в порядке программы в потоке t. В вашем примере это возможно, потому что порядок назначений не наблюдается. - person Durandal; 03.07.2014
comment
Что можно наблюдать с точки зрения JMM, что означает использование терминов из спецификации JMM? Как сделать b volatile сделать переупорядочение наблюдаемым с точки зрения действий/выполнений? - person AngryJuice; 03.07.2014
comment
Вы спрашиваете кругами. Volatile дает более сильные гарантии, чем просто порядок программы для потока t, как я изначально сказал в ответе, последний абзац. Что касается того, что можно наблюдать, представьте метод boolean isLoginCorrect(user, passwordHash). Это может сделать что-нибудь внутри. Любое преобразование, которое по-прежнему возвращает правильное логическое значение для аргументов, не наблюдается. Возьми? - person Durandal; 03.07.2014
comment
Мое беспокойство по поводу объяснений заключается в том, что вы пытаетесь описать формальную модель интуитивно понятным образом, чего я не ожидаю, поскольку я сам понимаю, как она работает на этом уровне. Я хочу вывести невозможность переупорядочивания формально из спецификации модели, и здесь недостаточно операторов «происходит до того», как это может быть нарушено, как вы сказали, если изменение не наблюдается. - person AngryJuice; 03.07.2014
comment
Хм, да, в основном, я пытаюсь разбить его, чтобы его было легко понять с моей (программистской) точки зрения. Если вы хотите сделать это официально, то это другой зверь. Вам нужно будет сформулировать все ограничения в вашем формализме и работать с этим (или принять формализм JMM). Но теперь вы глубоко погрузились в область анализа потока кода — классифицируйте каждое действие (инструкцию байт-кода) с точки зрения JMM как действие и используйте набор правил, чтобы определить, какие преобразования допустимы. Возможно, вам повезет, если вы прямо спросите об этом, но я предполагаю, что это слишком широко. - person Durandal; 03.07.2014

Похоже, вы совершаете распространенную ошибку, слишком много думая о низкоуровневых аспектах JMM в изоляции. Что касается вашего вопроса «как люди используют это на практике», если вы говорите о программисте приложений, он (она) будет использовать это на практике, не думая все время о барьерах памяти или возможных переупорядочениях.

Что касается вашего примера:

public void someMethod() {
  a = 2; // 1 
  b = 3; // 2  
}   

Учитывая, что a и b не-final, не-volatile.

Понятно, что в потоке w(a = 2) происходит раньше w(b = 3) в соответствии с порядком программы. Как компилятор/оптимизатор может быть уверен, что изменение порядка 1 и 2 не приведет к «незаконному выполнению» (строго с точки зрения модели)?

Здесь имеет неприятные последствия то, что вы сосредоточены на повторном заказе в изоляции. Во-первых, полученный код (оптимизация HotSpot, JIT-компиляция и т. д.) вообще не требует записи значений в динамическую память. Он может хранить новые значения в регистрах ЦП и использовать их оттуда в последующих операциях того же потока. Только при достижении точки эти изменения должны быть видны другим потокам, они должны быть записаны в кучу. Что может происходить в произвольном порядке.

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

И почему, если мы установим b изменчивым, он будет ?

Объявление b как volatile не гарантирует, что a и b будут записаны. Это еще одна ошибка, возникающая из-за концентрации внимания на барьерах памяти.

Давайте более абстрактно:

Предположим, у вас есть два одновременных действия, A и B. Для параллельного выполнения в Java существует несколько совершенно допустимых вариантов поведения, в том числе:

  • A может быть выполнен полностью до B
  • B может быть выполнен полностью до A
  • Все или части A и B работают параллельно

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

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

Чтобы продолжить ваш пример с b равным volatile: это означает, что если читающий поток читает b и читает значение 3, и только тогда он гарантированно увидит значение 2 (или даже более позднее значение, если есть другие записи) для a при последующих чтениях.

Таким образом, если JVM может доказать, что никогда не будет операции чтения для b, увидев записанное значение, возможно, потому, что весь экземпляр, который мы модифицируем, никогда не будет виден другому потоку, не произойдет ничего, прежде чем связь будет когда-либо установлена, в Другими словами, b в качестве volatile не влияет на разрешенные преобразования кода в этом случае, т. е. он также может быть переупорядочен или даже вообще не записываться в кучу.


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

person Holger    schedule 13.08.2014