Пункты памяти и стиль кодирования по Java VM

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

class Foo() { int a, b; }
static Foo theFoo;
void updateFoo(int newA, int newB) {
  f = new Foo();
  f.a = newA;
  f.b = newB;
  // HERE
  theFoo = f;
}
void readFoo() {
  Foo f = theFoo;
  // use f...
}

Мне все равно, читает ли мой читатель старый или новый Foo, однако мне нужно увидеть полностью инициализированный объект. IIUC. Спецификация Java говорит, что без барьера памяти в ЗДЕСЬ я могу увидеть объект с инициализацией f.b, но f.a еще не зафиксирован в памяти. Моя программа - это настоящая программа, которая рано или поздно переносит материал в память, поэтому мне не нужно сразу передавать новое значение theFoo в память (хотя это не помешает).

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

Как вы напишете его так, чтобы он был максимально читабельным?
Бонусные преимущества для версии Scala:)

Ответ 1

Краткие ответы на исходный вопрос

  • Если Foo является неизменным, просто создание полей final гарантирует полную инициализацию и постоянную видимость полей для всех потоков независимо от синхронизации.
  • Независимо от того, является или нет Foo неизменным, публикация через volatile theFoo или AtomicReference<Foo> theFoo достаточна для того, чтобы записи в его поля были видны для любого чтения нитей с помощью theFoo reference
  • Используя простое назначение theFoo, нити читателей никогда не гарантируют, что вы увидите какое-либо обновление.
  • По моему мнению, и на основе JCiP, "наиболее читаемый способ реализации барьера памяти" - AtomicReference<Foo>, с явной синхронизацией, входящей в секунду, и использование volatile, входящего в третью
  • К сожалению, мне нечего предложить в Scala

Вы можете использовать volatile

Я обвиняю вас. Теперь я подключен, я разбил JCiP, и теперь мне интересно, правильно ли написан какой-либо код, который я когда-либо писал. Фрагмент кода выше, по сути, потенциально несовместим. (Изменить: см. Раздел ниже в разделе Безопасная публикация с помощью volatile.) В потоке чтения также можно увидеть устаревшие (в данном случае любые значения по умолчанию для a и b) для неограниченного времени. Вы можете сделать одно из следующих действий, чтобы представить крайний край:

  • Опубликовать через volatile, который создает резидентный фронт, эквивалентный monitorenter (стороне чтения) или monitorexit (сторона записи)
  • Используйте поля final и инициализируйте значения в конструкторе перед публикацией
  • Представьте синхронизированный блок при записи новых значений в theFoo object
  • Использовать поля AtomicInteger

Это позволяет упорядочить порядок записи (и решает проблемы их видимости). Затем вам нужно рассмотреть видимость новой ссылки theFoo. Здесь volatile подходит - JCiP говорит в разделе 3.1.4 "Неустойчивые переменные" (и здесь переменная theFoo):

Вы можете использовать изменчивые переменные только при выполнении всех следующих критериев:
  • Записывает переменную, не зависящую от ее текущего значения, или вы можете убедиться, что только один поток когда-либо обновляет значение;
  • Переменная не участвует в инвариантах с другими переменными состояния; и
  • Блокировка не требуется по какой-либо другой причине во время доступа к переменной

Если вы сделаете следующее, вы будете золотыми:

class Foo { 
  // it turns out these fields may not be final, with the volatile publish, 
  // the values will be seen under the new JMM
  final int a, b; 
  Foo(final int a; final int b) 
  { this.a = a; this.b=b; }
}

// without volatile here, separate threads A' calling readFoo()
// may never see the new theFoo value, written by thread A 
static volatile Foo theFoo;
void updateFoo(int newA, int newB) {
  f = new Foo(newA,newB);
  theFoo = f;
}
void readFoo() {
  final Foo f = theFoo;
  // use f...
}

Простой и читаемый

Несколько человек на этом и других потоках (спасибо @John V) отмечают, что власти по этим вопросам подчеркивают важность документирования поведения синхронизации и допущений. JCiP подробно рассказывает об этом, предоставляет набор аннотаций, который можно использовать для документирования и статической проверки, а также вы можете посмотреть на JMM Cookbook для индикаторов о конкретных поведениях, которые потребуют документации и ссылок на соответствующие ссылки. Дуг Ли также подготовил список вопросов для рассмотрения, когда документирует поведение concurrency. Документация уместна, в частности, из-за беспокойства, скептицизма и путаницы вокруг проблем concurrency (на SO: "Был ли java concurrency цинизм зашел слишком далеко?" ). Кроме того, инструменты, такие как FindBugs, теперь предоставляют правила статической проверки, чтобы заметить нарушения семантики аннотации JCiP, например "Непоследовательная синхронизация: IS_FIELD-NOT_GUARDED" .

До тех пор, пока вы не подумаете, что у вас есть причина в противном случае, лучше всего перейти к наиболее читаемому решению, что-то вроде этого (спасибо, @Burleigh Bear), используя @Immutable и @GuardedBy аннотации.

@Immutable
class Foo { 
  final int a, b; 
  Foo(final int a; final int b) { this.a = a; this.b=b; }
}

static final Object FooSync theFooSync = new Object();

@GuardedBy("theFooSync");
static Foo theFoo;

void updateFoo(final int newA, final int newB) {
  f = new Foo(newA,newB);
  synchronized (theFooSync) {theFoo = f;}
}
void readFoo() {
  final Foo f;
  synchronized(theFooSync){f = theFoo;}
  // use f...
}

или, возможно, поскольку он чище:

static AtomicReference<Foo> theFoo;

void updateFoo(final int newA, final int newB) {
  theFoo.set(new Foo(newA,newB)); }
void readFoo() { Foo f = theFoo.get(); ... }

Когда это целесообразно использовать volatile

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

Фактически поиск google: "site: stackoverflow.com + java + volatile + keyword" возвращает 355 различных результатов. Использование volatile - это, в лучшем случае, неустойчивое решение. Когда это уместно? JCiP дает некоторые абстрактные указания (цитируется выше). Здесь я найду еще несколько практических рекомендаций:

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


Безопасная публикация через изменчивый

Следуя @Jed Wesley-Smith, кажется, что volatile теперь обеспечивает более сильные гарантии (с JSR-133) и более раннее утверждение "Вы можете использование volatile при условии, что опубликованный объект является неизменным" достаточно, но, возможно, не нужно.

Глядя на FAQ JMM, две записи Как работают последние поля под новым JMM? и Что делает volatile do? на самом деле не справляются вместе, но я думаю, что второй дает нам то, что нам нужно:

Разница в том, что сейчас нет дольше, так легко переупорядочить нормальное поле доступ вокруг них. Письмо в поле volatile имеет одинаковую память эффект как релиз монитора, и чтение из изменчивого поля имеет тот же эффект памяти, что и монитор приобретать. Фактически, поскольку новый модель памяти более жесткая ограничения на переупорядочивание изменчивых доступ к полям с другим полем доступ, неустойчивость или нет, что-либо который был виден в потоке A, когда он пишет в неустойчивое поле f видимый для потока B, когда он читает f.

Отмечу, что, несмотря на несколько перечитаний JCiP, соответствующий текст там не перескакивал, пока Джед не указал на это. Это на стр. 38, раздел 3.1.4, и он говорит более или менее то же, что и предыдущая цитата - опубликованный объект должен быть только эффективно неизменным, не требуются поля final, QED.

Старые вещи, предназначенные для подотчетности

Один комментарий: Любая причина, по которой newA и newB не могут быть аргументами для конструктора? Тогда вы можете полагаться на правила публикации для конструкторов...

Кроме того, использование AtomicReference, вероятно, устраняет любую неопределенность (и может купить вам другие преимущества в зависимости от того, что вам нужно сделать в остальной части класса...) Кроме того, кто-то умнее меня может сказать вам, если volatile решит это, но всегда кажется загадочным для меня...

В дальнейшем обзоре я считаю, что комментарий от @Burleigh Bear выше правилен --- (EDIT: см. ниже) вам действительно не нужно беспокоиться о заказе вне очереди, поскольку вы публикуют новый объект theFoo. В то время как другой поток, возможно, мог видеть несогласованные значения для newA и newB, как описано в JLS 17.11, этого здесь не может быть, потому что они будут переданы в память до того, как другой поток получит ссылку на новый f = new Foo() экземпляр, который вы создали... это безопасная разовая публикация. С другой стороны, если вы написали

void updateFoo(int newA, int newB) {
  f = new Foo(); theFoo = f;     
  f.a = newA; f.b = newB;
}

Забастовкa > Но в этом случае проблемы синхронизации достаточно прозрачны, и упорядочение - это наименьшее из ваших забот. Для некоторых полезных рекомендаций по volatile, ознакомьтесь с этой статьей developerWorks.

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

Ответ 2

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

Лично мне нравится иметь неизменяемые классы значений, так как их гораздо труднее использовать.