Пункты памяти и стиль кодирования по Java VM
Предположим, у меня есть статический сложный объект, который периодически обновляется пулом streamов и читается более или менее непрерывно в длинном streamе. Сам объект всегда неизменен и отражает последнее состояние чего-то.
class Foo() { int a, b; } static Foo theFoo; void updateFoo(int newA, int newB) { f = new Foo(); fa = newA; fb = newB; // HERE theFoo = f; } void readFoo() { Foo f = theFoo; // use f... }
Меня нисколько не волнует, видит ли мой читатель старый или новый Foo, однако мне нужно увидеть полностью инициализированный объект. IIUC, спецификация Java говорит, что без барьера памяти в ЗДЕСЬ я могу увидеть объект с инициализированным fb, но fa еще не зафиксирован в памяти. Моя программа – это настоящая программа, которая рано или поздно переносит материал в память, поэтому мне не нужно сразу передавать новое значение theFoo в память (хотя это не помешает).
Как вы считаете, наиболее читаемым способом реализации барьера памяти? Я готов заплатить небольшую цену за производительность, если потребуется. Я думаю, что я могу просто синхронизировать задание с Foo, и это сработает, но я не уверен, что для кого-то, кто читает код, очень важно, почему я это делаю. Я мог бы также синхронизировать всю инициализацию нового Foo, но это привело бы к большей блокировке, которая действительно необходима.
- Executors.newCachedThreadPool () по сравнению с Executors.newFixedThreadPool ()
- Безопасно ли читать указатель на функцию одновременно без блокировки?
- Контейнер Jboss Java EE и ExecutorService
- Как использовать ConcurrentLinkedQueue?
- Выбор лучшего списка параллелизма в Java
Как вы могли бы написать его так, чтобы он был максимально читабельным?
Бонусная наgradleа за версию Scala 🙂
- Что такое хороший шаблон для использования Глобального Mutex в C #?
- RxJava вместо AsyncTask?
- В чем разница между параллелизмом, параллелизмом и асинхронными методами?
- Как мне вызвать некоторый метод блокировки с тайм-аутом в Java?
- Кажется, что сервлет обрабатывает несколько одновременных запросов браузера синхронно
- Невозможно создать кэшированный пул streamов с ограничением размера?
- Безопасность платформы Entity Framework
- Зачем использовать ReentrantLock, если вы можете использовать синхронизированный (это)?
Краткие ответы на исходный вопрос
- Если
Foo
неизменен, просто создание полей final обеспечит полную инициализацию и постоянную видимость полей для всех streamов независимо от синхронизации. - Независимо от того, является ли
Foo
неизменным, публикация черезvolatile theFoo
илиAtomicReference
достаточна для обеспечения того, чтобы записи в его поля были видны для любого streamа, считываемого с помощью ссылкиtheFoo theFoo
- Используя обычное назначение для
theFoo
, нити читателей никогда не гарантированно будут видеть какое-либо обновление - По моему мнению, и на основе JCiP, «наиболее читаемым способом реализации барьера памяти» является
AtomicReference
, при этом явная синхронизация идет в секунду, а использованиеvolatile
вступает в третье - К сожалению, мне нечего предложить в Scala
Вы можете использовать volatile
Я виню тебя. Теперь я подключен, я разбил JCiP , и теперь мне интересно, правильный ли код, который я когда-либо писал. Фрагмент кода выше, по сути, потенциально несовместим. (Изменить: см. Раздел ниже «Безопасная публикация с помощью volatile.»). В streamе чтения также можно увидеть устаревшие (в этом случае любые значения по умолчанию для Вы можете сделать одно из следующих действий, чтобы ввести крайний край: a
и b
) для неограниченного времени.
- Публикация с помощью
volatile
, которая создает предыдущий край, эквивалентныйmonitorenter
(стороне чтения) илиmonitorexit
(запись) - Используйте
final
поля и инициализируйте значения в конструкторе перед публикацией - Ввести синхронизированный блок при записи новых значений в объект
theFoo
- Использовать поля
AtomicInteger
Это позволяет упорядочить порядок записи (и решает проблемы их видимости). Затем вам нужно обратиться к видимости новой ссылки theFoo
. Здесь volatile
уместна – JCiP говорит в разделе 3.1.4 «Неустойчивые переменные» (и здесь переменная – это theFoo
):
Вы можете использовать изменчивые переменные только при выполнении всех следующих критериев:
- Запись в переменную не зависит от ее текущего значения, или вы можете убедиться, что только один stream когда-либо обновляет значение;
- Переменная не участвует в инвариантах с другими переменными состояния; а также
- Блокировка не требуется по какой-либо другой причине во время обращения к переменной
Если вы сделаете следующее, вы будете золотыми:
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 для индикаторов о конкретных поведениях, для которых потребуется документация и ссылки на соответствующие ссылки. Дуг Ли также подготовил список проблем, которые следует учитывать при документировании поведения параллелизма . Документация уместна, в частности, из-за беспокойства, скептицизма и путаницы в вопросах параллелизма (на SO: «Был ли слишком циничный параллелизм java?» ). Кроме того, такие инструменты, как FindBugs , теперь предоставляют правила статической проверки, чтобы заметить нарушения семантики annotations JCiP, такие как «Непоследовательная синхронизация: IS_FIELD-NOT_GUARDED» .
До тех пор, пока вы не подумаете, что у вас есть причина в противном случае, лучше всего перейти к наиболее читаемому решению, что-то вроде этого (спасибо, @Burleigh Bear), используя annotations @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 theFoo; void updateFoo(final int newA, final int newB) { theFoo.set(new Foo(newA,newB)); } void readFoo() { Foo f = theFoo.get(); ... }
Когда целесообразно использовать volatile
Во-первых, обратите внимание, что этот вопрос относится к вопросу здесь, но был рассмотрен много, много раз на SO:
- Когда вы используете волатильность?
- Вы когда-нибудь использовали ключевое слово volatile в Java?
- Для того, что используется “volatile”
- Использование ключевого слова volatile
- Java volatile boolean vs. AtomicBoolean
Фактически, поиск google: «site: stackoverflow.com + java + volatile + keyword» возвращает 355 отличных результатов. Использование volatile
– это, в лучшем случае, неустойчивое решение. Когда это уместно? JCiP дает некоторые абстрактные указания (цитируется выше). Здесь я найду еще несколько практических рекомендаций:
volatile
можно использовать для безопасного опубликования неизменяемых объектов», который аккуратно инкапсулирует большую часть диапазона использования, которое можно ожидать от программиста приложения. volatile
наиболее полезен в алгоритмах без блокировки» суммирует другой class использования – специальные, блокирующие алгоритмы, которые достаточно чувствительны к производительности, чтобы заслужить тщательный анализ и проверку экспертом. Безопасная публикация через изменчивый
Следуя за @Jed Wesley-Smith , похоже, что volatile
теперь обеспечивает более надежные гарантии (с JSR-133), и более раннее утверждение «Вы можете использовать volatile
при условии, что опубликованный объект является неизменным» достаточно, но, возможно, не обязательно.
Рассматривая FAQ JMM, две записи. Как работают последние поля под новым JMM? и что делает volatile? на самом деле не справляются вместе, но я думаю, что второй дает нам то, что нам нужно:
Разница в том, что теперь уже не так легко переупорядочивать нормальные обращения к полям вокруг них. Запись в энергозависимое поле имеет тот же эффект памяти, что и релиз монитора, а чтение из энергозависимого поля имеет тот же эффект памяти, что и монитор. Фактически, поскольку новая модель памяти накладывает более строгие ограничения на переупорядочивание волатильных полевых доступов с другими доступными полями, волатильными или нет, все, что было видимо для streamа A, когда оно записывает в изменчивое поле f, становится видимым для streamа B, когда оно читает f.
Замечу, что, несмотря на несколько перечитаний JCiP, соответствующий текст там не перескакивал, пока Джед не указал на это. Это на стр. 38, раздел 3.1.4, и он говорит более или менее то же, что и предыдущая цитата – опубликованный объект должен быть только эффективно неизменным, не требуется никаких final
полей, QED.
Старые вещи, предназначенные для подотчетности
Один комментарий: Любая причина, по которой newA
и newB
не могут быть аргументами для конструктора? Тогда вы можете положиться на правила публикации для конструкторов …
Кроме того, использование AtomicReference
вероятно, устраняет любую неопределенность (и может купить вам другие преимущества в зависимости от того, что вам нужно сделать в остальной части classа …) Кроме того, кто-то умнее меня может сказать вам, но мне всегда кажется загадочным …
В дальнейшем обзоре я считаю, что комментарий от @Burleigh Bear выше правилен — (EDIT: см. Ниже), вам действительно не нужно беспокоиться об упорядочивании вне очереди, поскольку вы публикуете новый объект для theFoo
.В то время как в другом streamе можно было бы увидеть противоречивые значения для newA
и newB
как описано в JLS 17.11, этого не может быть здесь, потому что они будут переданы в память до того, как другой stream получит ссылку на новый f = new Foo()
экземпляр f = new Foo()
вы создали … это безопасная разовая публикация.С другой стороны, если вы написали
void updateFoo(int newA, int newB) { f = new Foo(); theFoo = f; fa = newA; fb = newB; }
Но в этом случае проблемы синхронизации достаточно прозрачны, и упорядочение – это наименьшее из ваших забот. Для некоторых полезных рекомендаций по летучести ознакомьтесь с этой статьей developerWorks .
Однако у вас может возникнуть проблема, когда отдельные streamи чтения могут видеть старое значение для theFoo
для неограниченного количества времени. На практике это редко случается. Однако JVM может быть разрешено кэшировать значение ссылки theFoo
в контексте другого streamа. Я совершенно уверен, что маркировка theFoo
мере theFoo
как volatile
будет обращаться к этому, как и любой синхронизатор или AtomicReference
.
Наличие неизменяемого Foo с конечными полями a и b решает проблемы видимости со значениями по умолчанию, но делает это также неустойчивым.
Лично мне нравится иметь неизменяемые classы ценностей, так как они намного сложнее использовать.