Удалёнка: опыт и лайфхаки
Реклама
Комментарии 34
НЛО прилетело и опубликовало эту надпись здесь
0
Там пример именно реордеринга на проце. См раздел «Опасности volatile»
0
Если я правильно понимаю, Parallel.Invoke дожидается окончания выполнения каждой из переданных задач. В JMM все действия, выполненные потоком happen-before успешного завершения join на этом потоке, потому все изменения будут видны.
0
Atomicity
Хотя многие это знают, считаю необходимым напомнить, что на некоторых платформах некоторые операции записи могут оказаться неатомарными. То есть, пока идёт запись значения одним потоком, другой поток может увидеть какое-то промежуточное состояние. За примером далеко ходить не нужно — записи тех же long и double, если они не объявлены как volatile, не обязаны быть атомарными и на многих платформах записываются в две операции: старшие и младшие 32 бита отдельно.
Поправьте меня, если я не прав. Читаю сейчас упомянутую книгу Java concurrency in Practice, где по поводу volatile говорилось, что оно никак не делает операции чтения атомарными, только делает значения видимыми между потоками. Т.е. в конкурентной борьбе при check-then-act операциях, проблема с long и double может по-прежнему проявиться.
+3
В стандарте явно сказано:
The load, store, read, and write actions on volatile variables are atomic, even if the type of the variable is double or long.


Вы, вероятно, путаете с тем, что не являются атомарными операции вроде инкремента, т.е.

class Foo {
    volatile int bar;

    void baz() {
        bar++; // not atomic!
    }
}
0
>В нашем примере оказывается, что достаточно сделать поле, запись в которое происходит последней, final, как всё магически заработает и без volatile и без синхронизации каждый раз:

Каким образом в данном примере гарантируется, что запись в final будет действительно последней, а не зареордерится куда-нибудь вверх?
0
Модель памяти это гарантирует. Всё, что поток X делает до выполнения операции записи в final поле (операции A), будет видно другому потоку Y, сохраняющему куда-то объект, содержащий это final поле (операция B). Какие-то из этих операций могут зареордериться по отношению друг к другу, но поток Y увидит сразу все изменения. Операция A не может зареордериться так, чтобы она оказалась раньше какой-то операции, которая находится до неё по program order. Вперёд, впрочем, передвинуться никто не запрещает.
0
jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html и у автора есть цикл статей + JLS/
Minor niggle: The read of ready doesn't just ensure that Thread 2 sees the contents of memory of Thread 1 up until it wrote to ready, it also ensures that Thread 2 sees the contents of memory of any other thread that wrote to ready up until that point
более правильное happens before в отношении volatile v java. также его привносит synchronized блок.
0
Не понял вас. По утверждению, приведённому в моей статье, то, что вы процитировали, тоже так.
0
final поля и Reflection
Запись final или volatile полей через Reflection, действительно, сопровождается memory-barrier'ом. Поэтому все требования JMM в этом случае тоже соблюдены.
Убедимся в этом, заглянув в исходники sun.reflect.*
    // ----- UnsafeFieldAccessorFactory.java -----
    boolean isQualified = isFinal || isVolatile;
    ...
    if (!isQualified) {
        return new UnsafeObjectFieldAccessorImpl(field);
    } else {
        return new UnsafeQualifiedObjectFieldAccessorImpl(field, isReadOnly);
    }

    // ----- UnsafeObjectFieldAccessorImpl.java -----
    public void set(Object obj, Object value) {
        if (isFinal) {
            throwFinalFieldIllegalAccessException(value);
        }
        ...
        unsafe.putObject(obj, fieldOffset, value);
    }

    // ----- UnsafeQualifiedObjectFieldAccessorImpl.java -----
    public void set(Object obj, Object value) {
        if (isReadOnly) {
            throwFinalFieldIllegalAccessException(value);
        }
        ...
        unsafe.putObjectVolatile(obj, fieldOffset, value);
    }


В свою очередь unsafe.putObjectVolatile(obj, fieldOffset, value) эквивалентен
    unsafe.putObject(obj, fieldOffset, value);
    membar();

0
Класс, спасибо, добавил в топик ссылку на комментарий.

Кстати, раз уж речь зашла о final полях, то мне по почте подсказывают, что я не прав относительно того, что happens-before для store финального поля и read объекта, это поле содержащего, — особенный happens-before, который не транзитивен с остальными. То есть, пример, который приведён в статье (сначала инициализировать обычные поля, потом одно final поле, и в результате обычные тоже будут видны) — некорректен.

В стандарте по этому поводу что-то есть, но у меня никак не получается осознать (и найти), что имеется в виду отношениями dereferences и mc. Не подскажете?
0
А, я уже и сам понял, и моё мнение не сошлось с мнением собеседника. Я тут приведу своё понимания, и если мне никто не укажет на ошибку, я обновлю статью.

В стандарте есть довольно мутная секция, рассказывающая об этом. Я понял её так:

freeze(f) — происходит после выхода (как нормального, так и в результате возникновения исключительной ситуации) из конструктора, создающего объект, содержащий f. Кроме того, freeze(f) происходит при каждом изменении значения f средствами reflection или чего-либо ещё.

a dereferences r, если (и):
a — операция чтения или записи поля объекта o, который был сконструирован в другом потоке
r — операция чтения адреса объекта o, происходящая в том же потоке, что и a

a mc b, если (или):
  • a — операция записи, которую видит операция чтения b
  • a dereferences b
  • b — запись адреса объекта, который не был конструирован в этом потоке, a — чтение адреса того же объекта в том же потоке


С этими определениями, если у нас есть операция записи w, операция freeze(f), действие a (не являющееся чтением финального поля), операция чтения r, которая читает поле f, и операция чтения r2, и при этом w happens-before freeze(f); freeze(f) happens-before a; a mc r; r dereferences r2, то:

w happens-before r2. При этом данное happens-before не транзитивно со всеми остальными.

Простыми словами, всё это значит вот что: операции записи переменных, находящихся в dereference chain финального поля, happen-before х чтение. Однако, операции, предшествущие этим операциям записи, уже не обязательно будут видны.
0
Я всё же был не прав, поскольку проглядел, что поле f — final. Потому предполагал, что раз w — запись любого поля, а не именно финального, она будет видна.

Сейчас добавлю в топик сноску.

Спасибо человеку по имени Ruslan Cheremin!
0
Reordering на Intel x86 хорошо описан в 'Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3A'.

Если коротко:

Есть program-ordering (другое определие strong-ordering) — модель исполнения reads и writes в порядке, определенном самой программой, и processor-ordering — модель исполнения reads и writes в порядке, улучшающим перворманс. Процессоры 486 и Pentium за небольшим исключением работают в модели program-ordering. Начиная с архитектуры P6 и заканчивая всеми текущими архитектурами x86, процессоры работают в модели processor-ordering. Имеются отличия в processor-ordering правилах для одно-процессорных систем и много-процессорных. Multi-core и включенный HyperThreading относится к много-процессорным системам.

Если JIT не изменит порядок операций, то assert в примере на reordering на x86 никогда не сработает, это пример как раз разобран в Developer’s Manual.

По поводу производительности volatile: на x86 работа с volatile реализуется с помощью FENCE инструкций, которые значительно влияют на производительность. Не знаю на сколько умны сейчас JIT-компиляторы по отношению к volatile переменным, используемым в одно-поточном коде, но они умеют удалять ненужные
синхронизации.
0
на x86 работа с volatile реализуется с помощью FENCE инструкций, которые значительно влияют на производительность

В HotSpot JVM на x86/amd64 запись volatile поля сопровождается инструкцией LOCK ADD [RSP], 0 что достаточно для эффективного membar'а вместо медленного MFENCE. Чтение volatile поля ничем не отличается от чтения обычного поля.

Что касается final полей, HotSpot с ними всегда работает как с обычными. Никаких дополнительных действий для обеспечения вышеупомянутой семантики final виртуальная машина не осуществляет, ровно по той причине, что на всех ныне поддерживаемых архитектурах (x86, SPARC, ARM, PPC), пример ReorderingSample никогда не свалится. Случая с Reflection это не касается, поскольку он относится с Class Libraries и реализован за пределами JVM.
+1
Небольшое уточнение:
Работа с final требует StoreStore барьера в конце конструктора. StoreStore барьер это no-op на x86 & SPARC, но таки требуется на ARM & PPC.
0
Выдержка из Java Concurrency In Practice:
Initialization safety makes visibility guarantees only for the values that are reachable through final fields as of the time the constructor finishes. For values reachable through non final fields, or values that may change after construction, you must use synchronization to ensure visibility.

Из этого можно сделать вывод что в третьем примере переменные question и answer также необходимо обозначить как final.

0
Хорошая статья поднимает интересные вопросы. Жаль проблема Word Tearing не была затронута. В спецификации JLS-JavaSE7 пункт 17.6 сказано что-то вроде того, что если процессор не может записывать в память отдельно байты, а может только слова, тогда при конкуррентном доступе данные запросто могут быть искажены. Насколько актуальна эта проблема?
0
Статья образец. Проработано несколько источников, отрецензирована и не в стиле «я вчера прочитал, спешу подельться».
Спасибо!
0
мда, крутая (имхо) статья собрала всего три десятка комментариев, что какбэ говорит об уровне современной многопоточной Java-разработки.
0
Помогите разобраться с Double-check Locking. У вас в статье упоминается синглтон и возможность его использования. В другой статье приводится аналогичный пример и говорится
Just define the singleton as a static field in a separate class. The semantics of Java guarantee that the field will not be initialized until the field is referenced, and that any thread which accesses the field will see all of the writes resulting from initializing that field.

Правильно ли я понимаю, что в случае с «обычными» полями данных, если началась инициализация, но не закончилась, одним потоком (который первым вошел в synchronized блок), то другой поток может увидеть ссылку на объект data, которая будет уже не null. Будет ли, действительно, решена проблема с использованием static?

Применительно к вашему примеру, будет ли это работать, если заменить volatile на static?

public class Keeper {
    private static Data data = null;

    // остальной код
}
0
Суть приведённой вами цитаты заключается в том, что поле не будет инициализированно до тех пор, пока к нему не обратятся в первый раз, т.е. инициализация статических полей ленивая. Чуть ниже в статье, на которую вы даёте ссылку, приведён код:

class HelperSingleton {
    static Helper singleton = new Helper();
}

Как вы можете заметить, присваивается при инициализации не null, а полноценный объект. И именно это присвоение имеет нужную семантику: все, кто обратятся к полю singleton гарантированно увидят один и тот же экземпляр класса Helper и все его поля, которые могли быть установлены в конструкторе.

Ваше исправление, конечно, работать не будет: всё, что оно гарантирует — это то, что первый, кто прочтёт data точно увидит null.

Про double checked locking хорошо писал TheShade: habrahabr.ru/post/143390/
0
Важно отметить, что, в отличие от того же C++, «из воздуха» (out-of-thin-air) значения никогда не берутся

Не могли бы прокомментировать вкратце, почему в C++ возникают значения «из воздуха»?
Только полноправные пользователи могут оставлять комментарии.  , пожалуйста.