Переупорядочивание заданий и добавление забора

Следующий код Java выглядит немного странно, потому что я упростил его до самого необходимого. Я думаю, что в коде есть проблема с порядком. Я смотрю на первую таблицу в поваренной книге JSR-133, и мне кажется, что обычное хранилище можно переупорядочить с помощью энергозависимого хранилища в change().

Может ли назначение m_normal в change() опережать назначение m_volatile? Другими словами, может ли get() вернуть null?

Каков наилучший способ решить эту проблему?

private          Object m_normal   = new Object();
private volatile Object m_volatile;

public void change() {
    Object normal;

    normal = m_normal;      // Must capture value to avoid double-read

    if (normal == null) {
        return;
    }

    m_volatile = normal;
    m_normal   = null;
}

public Object get() {
    Object normal;

    normal = m_normal;      // Must capture value to avoid double-read

    if (normal != null) {
        return normal;
    }

    return m_volatile;
}

Примечание. У меня нет контроля над кодом, в котором объявлено m_normal.

Примечание. Я работаю на Java 8.


person Nathan    schedule 12.02.2018    source источник


Ответы (2)


TL;DR: Друзья, не позволяйте друзьям тратить время на выяснение того, работает ли колоритный доступ в соответствии с желаниями Extreme Concurrency Optimist. Используйте volatile и спите спокойно.

Я смотрю на первую таблицу в поваренной книге JSR-133.

Обратите внимание, что полное название — «JMM Cookbook For Compiler Writers». Напрашивается вопрос: мы здесь создатели компиляторов или просто пользователи, пытающиеся разобраться в нашем коде? Я думаю, что последнее, поэтому нам действительно следует закрыть JMM Cookbook и открыть сам JLS. См. "Миф: Поваренная книга JSR 133 JMM Synopsis" и следующий раздел.

Другими словами, может ли get() вернуть null?

Да, тривиально, get() наблюдая за значениями по умолчанию для полей, не наблюдая за тем, что делал change(). :)

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

В основном вопрос заключается в том, существует ли допустимое выполнение, включающее read(m_normal):null --po/hb--> read(m_volatile):null, с чтением m_normal, наблюдающим за записью null в m_normal? Да, вот оно: write(m_volatile, X) --po/hb--> write(m_normal, null) ... read(m_normal):null --po/hb--> read(m_volatile):null.

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

Это правда, что операции с самим m_volatile обеспечивают некоторую семантику памяти: запись в m_volatile — это «выпуск», который «опубликует» все, что произошло до него, а чтение из m_volatile — это «получение», которое «получает» все опубликованное. Если вы точно сделаете вывод, как в этом посте, появится шаблон: вы можете тривиально переместить операции над программой «выпуск» вверх (в любом случае это было колоритно!), а вы можете тривиально переместите операции над программой «acquire» вниз (это тоже было пикантно!).

Эту интерпретацию часто называют "семантика мотеля тараканов", и она дает интуитивно понятный ответ. to: "Можно ли изменить порядок этих двух утверждений?"

m_volatile = value; // release
m_normal   = null;  // some other store

Ответ в соответствии с семантикой мотеля тараканов - «да».

Каков наилучший способ решить эту проблему?

Лучший способ решить эту проблему — с самого начала избегать колоритных операций и, таким образом, избежать всей неразберихи. Просто сделайте m_normal volatile, и все готово: операции над m_normal и m_volatile будут последовательно согласованы.

Будет ли добавлено значение = m_volatile; после m_volatile = значение; предотвратить назначение m_normal до назначения m_volatile?

Так что вопрос в том, поможет ли это:

m_volatile = value; // "release"
value = m_volatile; // poison "acquire" read 
m_normal   = null;  // some other store

В наивном мире только семантики мотеля тараканов это могло бы помочь: казалось бы, как будто яд ломает код движения. Но, поскольку значение этого чтения не наблюдается, оно эквивалентно выполнению без какого-либо подозрительного чтения, и хорошие оптимизаторы воспользуются этим. См. "Принятие желаемого за действительное: ненаблюдаемые летучие вещества имеют эффект памяти" ". Важно понимать, что переменные не всегда означают барьеры, даже если они есть в консервативной реализации, описанной в JMM Cookbook for Compiler Writers.

В стороне: есть альтернатива, VarHandle.fullFence(), которую можно использовать в подобном примере, но она ограничена очень влиятельными пользователями, потому что рассуждения с барьерами становятся граничащими с безумием. См. "Миф: барьеры — это нормально Ментальная модель" и «Миф: переупорядочение и запоминание».

Просто сделай m_normal volatile, и всем будет лучше спать.

person Aleksey Shipilev    schedule 12.02.2018
comment
У меня нет контроля над кодом, в котором объявлено m_normal. Кроме того, я работаю на Java 8. VarHandle недоступен. Можете ли вы предложить еще несколько решений? - person Nathan; 12.02.2018
comment
Будет ли решением добавление value.equals(null) или что-то в этом роде после чтения отравления? - person Nathan; 12.02.2018
comment
Это должно означать, что инопланетный код выполняет операции над m_normal, включая пикантную публикацию? Если это так, то нет надежного способа излечиться от этого, как нет способа отмыть уже случившуюся гонку. Вы можете надеяться, что что-то нарушает движение кода и непрозрачно для компилятора и аппаратного обеспечения, но это повязка. Я бы, вероятно, сделал невстроенный метод, который может получить доступ как к полям m_volatile, так и к полям m_normal, и вызвать его между хранилищами для m_volatile и m_normal и надеяться, что он обеспечивает достаточно зависимостей. - person Aleksey Shipilev; 12.02.2018
comment
@AlekseyShipilev Я читал ваши сообщения в блоге, смотрел, наверное, все ваши видео, которые смог найти, видел вас на конференциях, и это вступление здесь только для того, чтобы показать, как я боюсь писать этот комментарий. :) Тем не менее, здесь есть в основном три вещи: 1) Можно ли переупорядочить это m_volatile = normal; m_normal = null;, простой ответ да 2) Будет ли чтение изменчивой справки? нет, вы должны следить за написанным значением 3) поможет ли сделать его изменчивым? да, летучие вещества не могут быть переупорядочены между ними. Правильно ли я понимаю? - person Eugene; 13.02.2018
comment
Моим окончательным решением было отказаться от m_normal и использовать исключительно m_volatile. Это, конечно, разрешило переупорядочение JIT. - person Nathan; 13.02.2018
comment
@Евгений: 1) Верно; 2) Отчасти верно, потому что нам пришлось бы анализировать отдельно, поможет ли это. Я подозреваю, что вам нужно будет сделать управление зависимым от этого изменчивого чтения; 3) Правда. - person Aleksey Shipilev; 13.02.2018

// Must capture value to avoid double-read

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

Вставка изменчивого чтения между этими двумя:

m_volatile = normal;
tmp = m_volatile; // "poison read"
m_normal   = null;

неверно по другой причине, чем то, что Алексей Шипилев указал в своем ответе: в JMM нет утверждений об изменении порядков таких операций; устранение ненаблюдаемого «ядовитого чтения» никогда не изменяет порядок (никогда не устраняет барьеры) каких-либо операций. Фактическая проблема с "poison read" заключается в файле get().

Предположим, m_normal читает в get() наблюдает null. Какая m_volatile запись m_volatile читается в get() не разрешено в не synchronize-with? Проблема здесь в том, что он может появляться в общем порядке действий синхронизации до m_volatile записи в change() (переупорядочивается с m_normal чтением в get()), поэтому обратите внимание на начальный null в m_volatile, а не synchronize-with запись в m_volatile в change(). Вам понадобится «полный барьер» перед чтением m_volatile в get() - энергозависимом хранилище. Чего ты не хочешь.

Кроме того, использование только VarHandle.fullFence() в change() не решит проблему по той же причине: гонка в get() этим не устраняется.


PS. Объяснение, которое Алексей дал на https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#wishful-unobserved-volatiles слайд неверен. Там нет исчезающих барьеров, только разрешенные частичные заказы, где доступ к GREAT_BARRIER_REEF отображается как первое и последнее действия синхронизации соответственно.


Вы должны начать с предположения, что get() разрешено возвращать null. Затем конструктивно докажите, что это недопустимо. Пока у вас нет такого доказательства, вы должны предполагать, что это все еще может произойти.

Пример, где вы можете конструктивно доказать, что null не разрешено:

volatile boolean m_v;
volatile Object m_volatile;
Object m_normal = new Object();

public void change() {
  Object normal;

  normal = m_normal;

  if (normal == null) {
    return;
  }

  m_volatile = normal; // W2
  boolean v = m_v;     // R2
  m_normal   = null;
}

public Object get() {
  Object normal;

  normal = m_normal;

  if (normal != null) {
    return normal;
  }

  m_v = true;        // W1
  return m_volatile; // R1
}

Теперь начнем с предположения, что get() может вернуть null. Для этого get() должен соблюдать null в m_normal и m_volatile. Он может наблюдать null в m_volatile только тогда, когда R1 стоит перед W2 в общем порядке действий синхронизации. Но это означает, что R2 обязательно стоит после W1 именно в таком порядке, поэтому synchronizes-with оно. Это устанавливает happens-before между m_normal чтением в get() и m_normal записью в change(), поэтому m_normal чтению не разрешается наблюдать запись null (невозможно наблюдать записи, которые происходят после чтения) - противоречие. Таким образом, исходное предположение о том, что и m_normal, и m_volatile считывают null, неверно: по крайней мере одно из них будет наблюдать ненулевое значение, и метод вернет его.

Если у вас нет W1 в get(), в change() нет ничего, что могло бы вызвать перепад happens-before между m_normal чтением и m_normal записью, поэтому наблюдение за записью в get() не противоречит JMM.

person Sassa NF    schedule 19.02.2018