Ересь конечного поля Java: правильно ли я рассуждаю?

Недавно я начал изучать C# и сразу перешел к модели памяти. C# и Java имеют схожие (хотя, возможно, и не идентичные) гарантии безопасности потоков в отношении чтения и записи volatile полей. Но в отличие от записи в поля final в Java, запись в поля readonly в C# не дает какой-либо конкретной гарантии безопасности потока. Размышление о том, как работает безопасность потоков в C#, заставило меня усомниться в том, есть ли какое-то реальное преимущество в том, что final полей ведут себя так, как в Java.

Я узнал о предполагаемой важности final три года назад. Я задал этот вопрос и получил подробный ответ, который я принял. Но теперь я думаю, что это неправильно или, по крайней мере, неуместно. Я по-прежнему считаю, что поля должны быть final, когда это возможно, но не по общепринятым причинам.

Значение поля final гарантированно будет видно любому другому потоку после возврата из конструктора. Но ссылка на сам объект должна быть опубликована потокобезопасным способом. Если ссылка опубликована безопасно, гарантия видимости final становится избыточной.

Я рассматривал возможность того, что это как-то связано с public static полями. Но логически загрузчик классов должен синхронизировать инициализацию класса. Синхронизация делает безопасность потоков final избыточной.

Итак, я выдвигаю еретическую идею о том, что единственная реальная ценность final состоит в том, чтобы сделать неизменяемость самодокументируемой и самодостаточной. На практике закрытые поля, не являющиеся конечными, (и, в частности, элементы массива) полностью потокобезопасны, если они не изменяются после возврата из конструктора.

Я ошибся?

Изменить: перефразируя раздел 3.5 Java Concurrency in Practice,

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

Я понимаю, как final полей решают вторую проблему, но не первую проблему. На данный момент ответ с наибольшим количеством голосов утверждает, что первая проблема не является проблемой.

Изменить 2. Этот вопрос возникает из-за путаницы в терминологии.

Как и автор вопроса аналогичного вопроса, я всегда понимал термин "безопасная публикация" в том смысле, что внутреннее состояние объекта и ссылка на сам объект гарантированно видны другим потокам. В пользу этого определения Effective Java цитирует Goetz06, 3.5.3 в определении «безопасной публикации» как (курсив добавлен)

Перенос такой ссылки на объект из одного потока в другой

Также в пользу этого определения обратите внимание на то, что парафразированный выше раздел Java Concurrency in Practice относится к потенциально устаревшим ссылкам как к «неправильно опубликованным».

Как бы вы это ни называли, я не думал, что небезопасная публикация ссылки на неизменяемый объект может быть полезной. Но согласно этому ответу это возможно. (Приведенный здесь пример представляет собой примитивное значение, но тот же принцип можно применить и к ссылочным значениям.)


person Kevin Krumwiede    schedule 29.08.2017    source источник
comment
частные поля, не являющиеся конечными, (и, в частности, элементы массива) совершенно потокобезопасны, если они не изменяются после возврата конструктора - это неправильно. Если другой поток получит ссылку на ваш объект без правильной синхронизации, он может увидеть эти поля или значения массива как неинициализированные, даже если они были инициализированы в конструкторе.   -  person Erwin Bolwidt    schedule 29.08.2017
comment
@ErwinBolwidt Да, но у OP, похоже, сложилось впечатление, что ссылки никогда не должны передаваться без синхронизации.   -  person shmosel    schedule 29.08.2017
comment
@shmosel Думаю, ты прав. Также похоже, что OP считает, что синхронизация является абсолютной вещью, но правильные отношения JMM между потоками, использующими синхронизацию, достигаются только путем синхронизации на одном и том же мониторе (аналогично, правила, связанные с volatile, применяются только к такая же изменчивая переменная и т. д.), и загрузчик классов, выполняющий статическую инициализацию, не собирается каким-то образом заставлять все остальные потоки синхронизироваться на своем мониторе. (кстати, я имею в виду правильную синхронизацию в соответствии с терминологией JMM, которая может быть достигнута различными способами)   -  person Erwin Bolwidt    schedule 29.08.2017
comment
@ErwinBolwidt Я думаю, что OP прав в вопросе инициализации статического поля. См., например, stackoverflow.com/q/878577/1553851.   -  person shmosel    schedule 29.08.2017
comment
@ErwinBolwidt Я имею в виду, что это предложение будет истинным тогда и только тогда, когда ссылки публикуются безопасно.   -  person Kevin Krumwiede    schedule 29.08.2017
comment
На данный момент ответ, получивший наибольшее количество голосов, утверждает, что первая проблема не является проблемой. Не совсем так. Аргумент состоит в том, что это отдельное соображение, и его не следует смешивать со второй, гораздо более серьезной проблемой.   -  person shmosel    schedule 29.08.2017
comment
Обратите внимание, что автор вопроса, который вы связали (отличная находка, кстати!) Effective-immutable/27261575#comment14755004_7886577">отказался от своей интерпретации.   -  person shmosel    schedule 29.08.2017
comment
@shmosel Я видел это. Я все еще думаю, что терминология вводит в заблуждение, поскольку потенциальная невозможность публикации значения может считаться безопасной только при очень специфических обстоятельствах. Кроме того, надежность этого, вероятно, зависит от архитектуры. Недавно я узнал, что x86/x64 налагает более сильные гарантии видимости на более слабые гарантии Java, а ARM (например) — нет. Таким образом, квазибезопасная публикация неизменяемых объектов может работать на ПК и не работать на Android.   -  person Kevin Krumwiede    schedule 29.08.2017


Ответы (2)


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

Первое предложение неверно; поэтому второе не имеет значения. final может быть излишним при наличии других методов безопасной публикации, таких как синхронизация или volatile. Но смысл неизменяемых объектов в том, что они по своей сути потокобезопасны, то есть они будут отображаться в согласованном состоянии независимо от того, как опубликована ссылка. Таким образом, вам вообще не нужны эти другие методы, по крайней мере, в том, что касается безопасной публикации.

РЕДАКТИРОВАТЬ: OP правильно указывает, что есть некоторая двусмысленность в отношении термина «безопасная публикация». В этом контексте я имею в виду согласованность внутреннего состояния объекта. Проблема видимости, поскольку она влияет на ссылку, на мой взгляд, является обоснованной, но отдельной проблемой.

person shmosel    schedule 29.08.2017
comment
Я имею в виду, что, хотя сами объекты могут быть потокобезопасными, ссылки на них не являются безопасными автоматически. Например, было бы небезопасно, чтобы потоки взаимодействовали путем чтения и записи неконечного, энергонезависимого поля ссылки, независимо от встроенной безопасности потока типа поля. Если поток чтения увидит записанное значение ссылки, то да, объект, на который он ссылается, будет рассматриваться как полностью инициализированный. Но он может вообще не увидеть эталонное значение. - person Kevin Krumwiede; 29.08.2017
comment
Безопасность потоков может означать много разных вещей. Более мягкое определение может позволить отложенную видимость, но требует согласованности памяти. Все зависит от ваших конкретных требований. - person shmosel; 29.08.2017
comment
Я не знаю ни о какой задержке видимости. Насколько мне известно, значения либо гарантированно видны из-за отношения «происходит до» между записью и чтением, либо их видимость полностью неопределенна. - person Kevin Krumwiede; 29.08.2017
comment
Чтобы уточнить, я не знаю о какой-либо возможной согласованности гарантии в JMM. Это может быть характеристикой конкретной реализации или архитектуры. - person Kevin Krumwiede; 29.08.2017
comment
Я тоже не знаю о такой гарантии, но, насколько мне известно, на практике она обычно так и работает. Но какую бы терминологию вы ни предпочли, моя точка зрения такова, что видимость может не иметь существенного значения для правильности во всех случаях. - person shmosel; 29.08.2017
comment
@KevinKrumwiede В качестве практического примера рассмотрим блокировку с двойной проверкой. Обычно мы говорим, что поле должно быть volatile, или есть возможность увидеть ненулевую переменную с частично сконструированным объектом. С неизменяемыми объектами это невозможно. - person shmosel; 29.08.2017
comment
Я не считаю блокировку с двойной проверкой практическим примером чего-либо. До Java 1.5 он был общеизвестно сломан. После Java 1.5 стоимость неоспоримой блокировки была настолько низкой, что любое преимущество в производительности от блокировки с двойной проверкой стало крайне сомнительным. В любом случае, зачем вам использовать блокировку с двойной проверкой с неизменяемым объектом? - person Kevin Krumwiede; 29.08.2017
comment
Я явно про пост-1.5. Что касается его неоспоримости, это зависит от количества одновременных чтений. В любом случае, мое намерение состояло в том, чтобы предоставить реальный сценарий, в котором корректность не страдает из-за проблем с видимостью. Был ли этот сценарий непосредственно полезен для вас, на самом деле не имело значения. Что касается того, почему его можно использовать, это то же самое, что и всегда: лениво инициализировать какое-то значение потокобезопасным способом. Я не понимаю, почему неизменность значения что-то изменит. - person shmosel; 29.08.2017
comment
Я не понимаю, почему неизменность значения что-то изменит. - Именно поэтому я не понимаю актуальности блокировки с двойной проверкой для моего вопроса. - person Kevin Krumwiede; 29.08.2017
comment
Это меняет все в отношении безопасности потоков, как я уже объяснял. Я не понимаю, почему вы думаете, что DCL менее полезен для неизменяемых объектов. - person shmosel; 29.08.2017
comment
Нет, я думаю, что это одинаково (хотя и сомнительно) полезно как для неизменяемых, так и для фактически неизменяемых объектов, именно потому, что оно включает использование volatile для обеспечения видимости ссылки. Под безопасной публикацией я подразумеваю визуально согласованное состояние и видимость ссылки. Теперь я понимаю, что вы и другие понимаете, что это означает только первое. - person Kevin Krumwiede; 29.08.2017
comment
Я хотел сказать, что вам не понадобится volatile в этом случае. Видимость не является проблемой, потому что переменная не является нулевой. А непротиворечивость обеспечивается его неизменностью. И да, ваше понимание моего понимания точное. :) - person shmosel; 29.08.2017
comment
@shmosel volatile недостаточно для безопасной публикации, поскольку он не вводит необходимые барьеры памяти. На самом деле, даже если бы вы могли обойти это (а вы могли бы), каждый отдельный volatile вводил бы StoreLoad, которая является самой дорогой операцией на x86, в то время как final с ее StoreStore|LoadStore бесплатна. Я бы удалил эту часть volatile - person Eugene; 30.08.2017
comment
@ Юджин, я не совсем понимаю, на что ты отвечаешь. - person shmosel; 30.08.2017
comment
@shmosel мой плохой... final may be redundant in the presence of other safe publication techniques, like synchronization or volatile. volatile не должно быть в этом предложении. - person Eugene; 30.08.2017
comment
@ Юджин, я думаю, ты неправильно понял мой ответ и, возможно, вопрос. Обсуждение касается создания ссылки volatile или синхронизации доступа к ней, а не прямой замены final на volatile. - person shmosel; 30.08.2017
comment
@shmosel у меня сложилось впечатление, что вы говорите о безопасной публикации и как это делается через final; Извините, если я что-то не понял. - person Eugene; 30.08.2017
comment
@Eugene Моя точка зрения заключалась в том, что использование полей final внутри поля (т. е. сделать его должным образом неизменяемым) делает ненужным обеспечение безопасной публикации на эталонном уровне. Надеюсь, мы сможем договориться об этом. - person shmosel; 30.08.2017

Я прочитал ваш вопрос пару раз, и у меня все еще есть некоторые проблемы с его пониманием. Я просто попытаюсь дополнить другой ответ, который на самом деле правильный. final в Java - это все о переупорядочивании (или происходит раньше, как вы это называете).

Во-первых, это гарантируется JLS, см. Окончательная семантика поля. В примере вы можете видеть, что одно конечное поле гарантированно будет корректно отображаться другими потоками, а другое — нет. JLS правильный, но в текущей реализации достаточно одного поля final — читайте дальше.

За каждой записью в финальное поле внутри конструктора следуют два memory barriersStoreStore и LoadStore (отсюда и название «происходит до»); потому что сохранение в конечном поле произойдет до чтения в то же поле - гарантировано через барьеры памяти.

Но текущая реализация этого не делает — это означает, что барьеры памяти не возникают после каждой записи в final — они возникают в конце конструктора, см. это. Вы увидите эту важную строку:

 _exits.insert_mem_bar(Op_MemBarRelease, alloc_with_final());

Op_MemBarRelease является фактическим барьером LoadStore|StoreStore, поэтому в текущей реализации достаточно, чтобы одно поле было окончательным, чтобы все остальные также были безопасно опубликованы. Но, конечно, делайте это на свой страх и риск.

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

Обратите внимание, что публикация ссылок не вызывает беспокойства просто потому, что JLS говорит: Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values..

person Eugene    schedule 30.08.2017
comment
Главное, к чему я пришел, это то, что барьер памяти, о котором вы говорите, гарантирует только то, что значения полей объекта видны другим потокам. Это не гарантирует, что значение ссылки на объект будет видно другим потокам. (Атомарные ссылки также не имеют ничего общего с их видимостью.) Я был прав насчет того, что произойдет, если ссылка не будет безопасно опубликована. Я ошибался в том, что небезопасно опубликованные ссылки всегда ошибочны. - person Kevin Krumwiede; 30.08.2017
comment
@KevinKrumwiede ах! теперь это имеет смысл - здесь вы говорите о двух разных вещах Safe Initialization и Safe Publication. Действительно, для безопасной публикации вам понадобится ссылка, например, volatile. Здесь есть намного больше: shipilev.net/blog/2014/safe-public- строительство - person Eugene; 30.08.2017
comment
@KevinKrumwiede и, кстати, это будет все еще связано с барьерами памяти, поскольку volatile введет барьеры памяти, необходимые для того, чтобы действия, выполненные до записи volatile, были видны после чтения volatile; таким образом, сделать ссылку volatile было бы достаточно. - person Eugene; 30.08.2017
comment
Правильно. Частично меня смущало то, что в некоторых источниках термин «безопасная публикация» используется для обозначения безопасной инициализации, а не не того, что я считаю безопасной публикацией. - person Kevin Krumwiede; 30.08.2017
comment
@KevinKrumwiede, в таком случае вам следует изменить название вопроса, чтобы отразить это - вы могли бы помочь многим людям в будущем искать именно это. Спасибо - person Eugene; 31.08.2017