class AtomicInteger { private volatile int value; public AtomicInteger(final int value) { this.value = value; } .... } | |
---|---|
AtomicInteger sharedRef; | |
Thread 1 | Thread 2 |
sharedRef = new AtomicInteger(42); | if( sharedRef != null ) { System.out.println(sharedRef.get()); } |
- Может ли этот код вывести 0?
- ...Если может — то как?
Сможете ли вы теперь теми же глазами смотреть на AtomicInteger? - ...Если нет — то почему?
P.S. Я, если честно, точного ответа не знаю. Хотя пара правдоподобных вариантов у меня есть.
Если, не ошибаюсь, то из-за того что sharedRef не volatile, т.е. запись ссылки и её чтение не связаны никакими ребрами happens before, а value не объявлен как final, то есть вероятность того, что второй поток увидит ссылку на sharedRef, но вот поле value всё ещё будет содержать defaultValue, т.е. 0. И да, тут скорее конечно надо другими глазами не на AtomicInteger смотреть, а на new.
ОтветитьУдалитьНу насчет новыми глазами смотреть на конструкторы -- это я уже писал. Только когда это конкретно к AInteger применяется -- выглядит иначе, поскольку атомарные типы интуитивно воспринимаются как полностью потокобезопасные.
УдалитьМоя первая версия была именно такая. Однако вот один очень авторитетный человек считает, что AI является полностью безопасным для любой публикации. Есть ли у него основания так считать?
Подумал-поразмыслил. Возможно volatile поле влияет на реордеринг в пользу атомика, но механизм мне не ясен в этом случае. Взгляд зацепился за final параметр, но в отличие от final поля эффекта никакого от него не будет IMHO. Ну и совсем бессовестное предположение, что JVM знает об атомиках и не позволяет их небезопасных публикаций.
УдалитьНет, никакого специального читерства здесь нет -- интринсики JVM не привносят дополнительной семантики по сравнению с кодом, который они заменяют -- только оптимизацию. Все честно по JMM
УдалитьТогда, я честно расписываюсь в своём невежестве и неумении интерпретировать спецификацию JMM, по крайней мере для этого случая) Было бы любопытно узнать ваши объяснения с крокетом и блудницами.
УдалитьГлавное не торопиться посыпать голову пеплом. Даг Ли _каждый_ раз плачется, что вообще никто не умеет на регулярной основе строить корректные доказательства на чистой JMM :)
УдалитьЯ, думаю, что мы никодгда не получил ноль, только если пометим поле sharedRef как final. По крайней мере я всегда стараюсь так делать, когда с атомиками работаю, мне почему-то так спокойнее %)
ОтветитьУдалитьКстати, а почему в твоем примере поле констуктора final? Вроде в моих сорсах оно так не помечено. Ты туда его нарошно поставил?
final для локальных переменных никакой memory семантики не несет. Я просто сам привык так писать, вот и написал.
УдалитьПубликация через final ссылку -- это безопасная публикация. Здесь, конечно, никакого шухера не будет. Но вопрос-то в том -- безопасно ли публиковать атомики небезопасно )
Ах, да не ответил почему ноль-то можем получить.
ОтветитьУдалитьКод Thread1 по идее можно переписать так.
localRef = new AtomicInteger();
localRef.value = 42; // volatile store
sharedRef = localRef;
Какие же ограничения нагладываются volatile? Читаем тут один абзац: http://developer.android.com/training/articles/smp.html
Non-volatile accesses may be reorded with respect to volatile accesses in the usual ways, for example the compiler could move a non-volatile load or store “above” a volatile store, but couldn’t move it “below”. Volatile accesses may not be reordered with respect to each other. The VM takes care of issuing the appropriate memory barriers.
Т.е. строочка "sharedRef = localRef;" вполне легально "could move above a volatile store" т.е. сточки "localRef.value = 42" и мы получаем ноль.
Очень даже может быть, что на текущих x86 этой оптимизации никогда не происходит, т.е. такого быть в природе не может на большинстве конфигураций, но тут я уж точно не силен.
Вот почему такого эффекта не будет если пометить sharedRef как final, я объяснить не могу. Но я уверен, что JVM, начиная с версии 5.0, сделает все возможное и невозможное, чтобы в случае с final мы никогда не увидели 0.
Хорошее рассуждение, я тоже так сначала рассуждал. Однако -- теперь я уже практически уверен -- нет, 0 получить здесь нельзя, если реализация JVM соответствует JMM. Вопрос -- почему.
УдалитьУ Далвика, кстати, все же другая модель памяти, насколько я помню. Возможно, что в его модели как раз 0 возможен.
Ты имеешь ввиду реализация JMM другая, или сама JMM другая?
УдалитьРуслан, поясни все же откуда у тебя такая уверенность. Я просто открываю мануал от DL http://g.oswego.edu/dl/jmm/cookbook.html и в первой же табличке "Can Reorder" вижу, что если volatile store первая операция, а normal store вторая операция (т.е. наш случай, согласно развернотому коду выше в комментах), то запрета на реордеринг нет.
УдалитьЯ имею в виду у Далвика спецификация другая. В частности, насколько я помню, у них volatile больше похож на наш lazySet.
УдалитьВсе эти рассуждения -- про "может переставляться" и "не может переставляться" -- это не более, чем упрощенные эвристики. Они могут работать в большинстве случаев (и работают), но надо помнить, что всегда есть тонкие случаи, и только спецификация изначально истинна, а все остальное -- лишь тень от света, на стене бытия
Ну вот здесь http://stackoverflow.com/questions/4588076/is-dalviks-memory-model-the-same-as-javas, чувак, который работает над далвиком утвреждает, что, начиная с версии 3.0 у них практически все согласно JSR-133.
УдалитьА ты спецификацию умеешь интерпретировать, чтобы ее конкретно на этот случай спроецировать?
Или можно проще поступить. Взять запустить данный код, посмотреть во что он скомпилится на наличие барьеров памяти. Если таковой в нужном месте не обнаружится, то на твоей платформе можно ожидать нолик.
Ну, вобщем, с нетерпением жду от тебя подробностей решения данного квеста :)
Я не очень в курсе относительно Далвика, могу ошибаться. Некоторое время назад -- еще в яндексе -- я услышал что у них несколько ослабленная модель памяти, отложилось в памяти. Возможно, они уже поправились.
УдалитьИнтерпретировать спецификацию "вообще" я не буду браться. Но в этом конкретном случае у меня получилось построить доказательство. Там используются несколько непривычные свойства JMM, поэтому я не вполне был уверен в корректности. Но еще вчера авторитетный человек (c) подтвердил, что таки-да, именно так и именно поэтому.
Скомпилировать -- это слишком грубо. Например, формально lazySet в данном случае не даст тебе необходимых гарантий -- но фактически сгенерированный код на x86 их таки даст
safe publication
ОтветитьУдалитьThread 2 не может получить ссылку на AtomicInteger до того как он окончательно сконструирован. Конструктор AtomicInteger нигде не утекает ссылками на себя. Поэтому в момент присвоения значение в sharedRef AtomicInteger всегда инициализирован. Ну, а volatile на value гарантирует видимость значения в других потоках.
>Thread 2 не может получить ссылку на AtomicInteger до того как он окончательно сконструирован.
УдалитьПочему, собственно, не может? Кто ему помешает?
Прошу прощения. Упустил из вида коментарий про небезопасную публикацию sharedRef. Теперь стало интересее.
УдалитьРуслан,поясни плз "Почему, собственно, не может? Кто ему помешает?"
УдалитьВедь в предъявленном коде описан конструктор, и видно что утечки в нём нет.
В JSR-133 есть такие строки:
ОтветитьУдалитьA write to a volatile field happens-before every subsequent read of that volatile
не является ли это ответом?
Ключевое слово - subsequent. В исходном примере нет явных гарантий, что чтение произойдёт после записи.
УдалитьНу скажем так -- ответом это не является. Частью ответа -- возможно.
Удалить"In the Java Memory Model a volatile field has a store barrier inserted after a write to it and a load barrier inserted before a read of it. Qualified final fields of a class have a store barrier inserted after their initialisation to ensure these fields are visible once the constructor completes when a reference to the object is available."
УдалитьНе приводит ли store barrier после volatile write к такому же эффекту как и гарантированая видимость final поля?
Откуда цитата? По-моему, все как раз наоборот -- StoreStore барьер должен быть _до_ волатильной записи, а LoadLoad _после_ волатильного чтения. И, на самом деле, для волатильной записи нужен (рекомендуется) еще StoreLoad барьер
УдалитьНо в данном случае это не важно, потому что барьеры -- это особенность реализации. Доказательства корректности через них не ведутся
Так через JMM и нельзя доказать корректность. С точки зрения исключительно JMM результат может быть 0.
УдалитьХорошо, вы меня почти уговорили )
Удалитькстати, а вот таки интересный пример из жизни насекомых
ОтветитьУдалитьCHM:
static final class HashEntry {
final K key;
final int hash;
volatile V value;
final HashEntry next;
HashEntry(K key, int hash, HashEntry next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
}
в CHM, как известно, нельзя положить пару key-null
однако CHM$Segment.get как бы намекает на что-то
/**
* Reads value field of an entry under lock. Called if value
* field ever appears to be null. This is possible only if a
* compiler happens to reorder a HashEntry initialization with
* its table assignment, which is legal under memory model
* but is not known to ever occur.
*/
V readValueUnderLock(HashEntry e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
/* Specialized implementations of map methods */
V get(Object key, int hash) {
if (count != 0) { // read-volatile
HashEntry e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
О чём это нам хотел сказать Doug Lea ?
Прям как spurious wake up. Он есть. Но никто никогда не видел (вроде только на солярке бывает).
Удалитьнет, здесь именно тот случай, о котором и спрашивает Руслан - по сути вопроса HashEntry ничем не отличается от AtomicInteger
УдалитьВерно. Именно тот случай. Мне просто вот это понравилось:
Удалитьwhich is legal under memory model, but is not known to ever occur
В моих сорсах нашел еще javadoc к HashEntry:
Удалить* Because the value field is volatile, not final, it is legal wrt
* the Java Memory Model for an unsynchronized reader to see null
* instead of initial value when read via a data race. Although a
* reordering leading to this is not likely to ever actually
* occur, the Segment.readValueUnderLock method is used as a
* backup in case a null (pre-initialized) value is ever seen in
* an unsynchronized access method.
По сути тот же комментарий, что Володя привел, только другими словами.
Т.е. в javadoc пишут, что это легально с точки зрения JMM получить неиницилизированное значение, хотя в реальности это нигде и не случается. Руслан же на сколько я понял все же утверждает, что зря они соломку подстилают, не может там никто увидеть неинициализированного значения. Поправьте меня, пожалуйста, если я все же что-то не так понял.
!flood mode on
УдалитьRuslan. Have a fresh view of JMM.
!flood mode off
Возможно, этот код остался в CHM еще со времен, когда j.u.c был реализован под 1.4.
Удалитьой вряд ли, если посмотреть и сравнить backport-util-concurrent ...
УдалитьЭтот комментарий был удален автором.
ОтветитьУдалитьЧет у меня подозрение что дело все же в volatile поле. Т.е. переменная sharedRef получит ссылку на объект AtomicInteger когда выполнится конструтор, а так как выполнится конструктор то произойдет запись в volatile поле value. И в итоге получаем hb записи value и чтение. Результат ноль не увидим.
ОтветитьУдалитьОтвет: нет не возможно.
ОтветитьУдалитьКод для пояснения:
public class A {
B bRef;
public static void main(String[] args) {
A a = new A();
a.bRef = new B(a,42);
}
}
class B{
volatile int value;
public B(A a,final int c) {
this.value=c;
//будет null. ссылка ещё не запаблишилась.
System.out.println(a.bRef);
}
}
Запис volatile переменной happens-before паблишинга ссылки на B. Поэтому так зареордериться не сможет.
В итоге если bRef!=null то value уже не 0.
Нет, ваше объяснение не верно, хотя само утверждение конечно верно. program order, который согласован с HB, не распространяется на другие потоки сам по себе. То, что запись volatile упорядочена до записи ссылки на созданный объект не означает, что запись ссылки упорядочена с чтением этой ссылки в другом потоке.
УдалитьВ следущем посте подробно разбирается почему все-таки утверждение верно.
Прочитал ваш следующий пост.
УдалитьСобственно не нашёл принципиальной разницы в логике. Возможно я не совсем полно описал ход мыслей. Но так или иначе всё сводиться к пунку 8 из вашего следующего поста, о чем я и пытался сказать.
Принципиальная разница в логике, которую вижу я, состоит в том, что у вас ни разу не звучало ни total synchronization order, ни, хотя бы, synchronized-with. А это принципиально для доказательства -- если вы, конечно, его поняли. Ваше рассуждение переносится на случай не-volatile поля совершенно без изменений. Но это точно не верно, потому что без volatile доказательство работать не будет.
УдалитьЭто лишний раз доказывается вашим примером -- тот код, который вы приводите, демонстрирует всего лишь intra-thread semantics, которая совершенно не зависит от наличия или отсутствия volatile.
Доказательство(если это можно так назвать, скорей объяснение) не переноситься на случай без volatile поля, так как будет возможен реордеринг. В случае с volatile реордеринг записи в переменную и паблишинг ссылки не возможен. Так как паблишинг ссылки атомарен, то в другом потоке мы можем увидить либо null либо ссылку на наш объект. Соответственно полседующий read volatile увидит 42, так как ссылка не могла быть запаблишина до volatile write.
УдалитьСкажите, откуда вы взяли, что нельзя переставить обычную запись, идущую после волатильной, до этой волатильной?
Удалить>>ваше объяснение не верно, хотя само утверждение конечно верно
ОтветитьУдалитьРуслан, неверно ни утверждение, ни ваше доказательство http://cheremin.blogspot.ru/2013/02/jmm-solution.html :)
В вашем доказательстве ошибочно следующее утверждение:
"Поскольку (2) и (6) — synchronized actions, то из ![ (2) sw (6) ] => [(6) sw (2) ] => [(6) hb (2)]"
Ошибочно оно потому, что SW - частичный порядок, и потому из ![ (2) sw (6) ] не следует [(6) sw (2) ]. Единственный полный порядок определяемый JMM - это SO, но ведь тут речь не про него.
Алексей Шипишёв писал на конкаренси интерест аналогичный вопрос (http://cs.oswego.edu/pipermail/concurrency-interest/2013-November/011951.html) со своим обоснованием, где допустил похожую ошибку.
А вот и обоснование возможности прочитать 0, в котором пока никто ошибки не нашёл: http://cs.oswego.edu/pipermail/concurrency-interest/2013-November/011954.html
Кроме того, утверждение, что 0 прочитать возможно, "заверено печатью" Дага Ли:http://cs.oswego.edu/pipermail/concurrency-interest/2013-November/011966.html
Как раз в феврале мы с Алексеем и обсуждали этот вопрос -- ему в приватной переписке ДЛ упомянул, что это верно, но доказательства не привел. На пару мы придумали вот это доказательство, про которое ДЛ сказал "вчерне сойдет". Когда я уже готовил доклад для JPoint я обнаружил косяк, на который вы ссылаетесь, поэтому в моем докладе этого примера уже не было (а как я его хотел...). А в ноябре у Алексея таки дошли руки попытаться таки "починить" доказательство с использованием commitment procedure, и он вышел в c-i, откуда и появился тред, на который вы ссылаетесь. Такая вот история :)
Удалитьвот это переплетение событий! :)
Удалить