Публикация кажется несколько хитрой концепцией, и способ, которым она объяснена в параллельности Java на практике , мне не сработал (в отличие от многих других многопоточных концепций, описанных вэта замечательная книга ).
Имея в виду вышеизложенное, давайте сначала разберемся с некоторыми более простыми частями вашего вопроса.
, когда вы говорите допустим, что thread1 заканчивает первым - как вы узнали бы это?или, если быть более точным, как thread2"узнает" это?насколько я могу судить, это могло бы быть возможно только с некоторой синхронизацией, явной или не такой явной, как в потоке join (см. JLS - 17.4.5 Порядок Happens-before * ).Код, который вы предоставили до сих пор, не дает достаточных подробностей, чтобы сказать, имеет ли это место
, когда вы заявляете, что thread1 обновит кэшированное состояние - как бы thread2"знаете" что?с предоставленным вами фрагментом кода для thread2 кажется совершенно возможным (но не гарантированным), что вы никогда не узнаете об этом обновлении
при указании thread2 ... переопределит состояние что здесь означает override ?В примере кода GlobalStateCache нет ничего, что могло бы как-то гарантировать, что thread1 когда-нибудь заметит это переопределение .Более того, предоставленный код не предлагает ничего, что каким-либо образом навязывало бы отношение «произойдет до» обновлений из разных потоков, так что можно даже предположить, что переопределение может произойти наоборот, понимаете?
последнее, но не менее важное, формулировка неизменное состояние звучит для меня довольно размыто.Я бы сказал опасно нечетко, учитывая эту сложную тему.Поле состояние является изменяемым, его можно изменить, хорошо, вызвав метод updateState верно?Из вашего кода я бы скорее пришел к выводу, что экземпляры класса MyImmutableState предполагаются неизменяемыми - по крайней мере, так говорит мне имя.
С учетом всего сказанного выше, чтогарантированно будет видимым с кодом, который вы предоставили до сих пор?Боюсь, не очень ... но, может быть, лучше, чем ничего.Я вижу это ...
Для thread1 гарантируется, что перед вызовом updateState он увидит либо null , либо правильно построенный (действительный ) объект обновлен из thread2.После обновления гарантированно виден любой из правильно построенных ( valid ) объектов, обновленных из thread1 или thread2.Обратите внимание, что после этого обновления thread1 гарантированно не увидит null для самого JLS 17.4.5, на который я ссылаюсь выше ( "... x и y - действия того же потока, и x предшествует yв программном порядке ... ")
Для резьбы 2 гарантии очень похожи на описанные выше.
По сути, все это гарантируется с помощью предоставленного вами кодав том, что оба потока будут видеть null или один из правильно построенных ( valid ) экземпляров MyImmutableState класса.
Выше гарантии могут выглядеть незначительнымина первый взгляд, но если вы просмотрите одну страницу выше страницы с цитатой, которая вас смутила («Неизменяемые объекты можно безопасно использовать и т. д.»), вы найдете более полезный пример в 3.5.1.Неправильная публикация: когда плохие объекты испортятся .
Да, объект, который сам по себе неизменен, не гарантирует его видимости, но, по крайней мере, гарантирует, что объект не "взорвется изнутри", как в примерепредусмотрено в 3.5.1:
public class Holder {
private int n;
public Holder(int n) { this.n = n; }
public void assertSanity() {
if (n != n)
throw new AssertionError("This statement is false.");
}
}
Комментарии Гетца для приведенного выше кода начинаются с объяснения истинных проблем как с изменяемыми, так и с неизменяемыми объектами,
... мы говорим, что Holder был неправильно опубликован .Две вещи могут пойти не так с неправильно опубликованными объектами.Другие потоки могли видеть устаревшее значение для поля владельца и, таким образом, видеть нулевую ссылку или другое более старое значение, даже если значение было помещено в держатель ...
... затем он погружается вчто может произойти, если объект изменяемый ,
... Но, что еще хуже, другие потоки могут видеть значение обновления для ссылки на владельца, но устаревшие значения для состояниядержателя.Чтобы сделать еще менее предсказуемым , поток может увидеть устаревшее значение при первом чтении поля, а затем более актуальное значение в следующий раз, поэтому assertSanity может выдать AssertionError .
Выше «AssertionHorror» может показаться нелогичным, но вся магия исчезнет, если вы рассмотрите сценарий, подобный приведенному ниже (полностью допустимый для модели памяти Java 5 - и по хорошей причине, кстати):
вызывает поток 1 sharedHolderReference = Holder (42);
первый поток потока 1 заполняет n поле со значением по умолчанию (0) затем назначит его в конструкторе, но ...
... но планировщик переключается на thread2,
sharedHolderReference из потока 1 становится видимым для потока 2, потому что, скажем, потому что почему бы и нет?возможно, оптимизирующий компилятор хот-спота решил, что сейчас самое подходящее время, чтобы
thread2 считал обновление sharedHolderReference со значением поля, все еще равным 0 между прочим
thread2 вызывает sharedHolderReference.assertSanity ()
thread2 считывает значение левой стороны , если инструкция в assertSanity , который, ну, тогда он будет читать значение правой стороны, но ...
... но планировщик переключается обратноto thread1,
thread1 завершает присвоение конструктора, приостановленное на шаге 2 выше, установив n значение поля 42
значение 42 в поле n из потока 1 становится видимым для потока 2, потому что, скажем, почему бы и нет?может быть, оптимизирующий компилятор хот-спота решил, что это подходящее время для
, а затем, через некоторое время, планировщик переключается обратно на thread2
thread2 продолжается с того места, где он был приостановлен на шаге 6 выше, то есть он читает правую часть оператора if , что, в общем, 42 теперь
упс, наша невинная if (n! = N) внезапно превращается в if (0! = 42) , что ...
... естественно выбрасывает AssertionError
Насколько я понимаю, безопасность инициализации для неизменяемых объектов просто гарантирует, что выше не будетбывает - не больше ... и не меньше