Барьеры памяти и стиль кодирования на Java VM - PullRequest
26 голосов
/ 19 октября 2010

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

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:)

Ответы [ 2 ]

45 голосов
/ 19 октября 2010

Короткие ответы на оригинальный вопрос

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

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

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

  • Публикация через volatile, в результате чего создается край перед событием, эквивалентный monitorenter (сторона чтения) или monitorexit (сторона записи)
  • Используйте final поля и инициализируйте значения в конструкторе перед публикацией
  • Ввести синхронизированный блок при записи новых значений в theFoo объект
  • Использовать 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 для индикаторов о конкретных поведениях, которые могли бы требовать документацию и ссылки на соответствующие ссылки. Даг Ли также подготовил список вопросов, которые следует учитывать при документировании поведения параллелизма . Документация уместна, особенно из-за беспокойства, скептицизма и путаницы, связанных с проблемами параллелизма (на SO: "Не зашел ли цинизм параллелизма Java слишком далеко?" ). Кроме того, такие инструменты, как 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 + ключевое слово" возвращает 355 различных результатов. Использование volatile в лучшем случае является нестабильным решением. Когда это уместно? JCiP дает некоторое абстрактное руководство (упомянутое выше). Я соберу несколько практических рекомендаций здесь:

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


Безопасная публикация через volatile

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

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

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

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

Старые вещи, сохраняемые для ответственности

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

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

В дальнейшем рассмотрении, я считаю, что комментарий от @Burleigh Bear выше является правильным --- (РЕДАКТИРОВАТЬ: см. Ниже) Вам на самом деле не нужно беспокоиться о неупорядоченном порядке, так как вы публикуем новый объект для 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;
}

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

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

4 голосов
/ 19 октября 2010

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...