Я работаю ежедневно с моделью памяти Java уже несколько лет. Я думаю, что я хорошо понимаю концепцию расчётов данных и различные способы их избежать (например, синхронизированные блоки, изменчивые переменные и т.д.). Тем не менее, все еще есть что-то, что я не думаю, что полностью понимаю модель памяти, так как конечные поля классов должны быть потокобезопасными без дальнейшей синхронизации.
Итак, согласно спецификации, если объект правильно инициализирован (то есть ссылка на объект не экранируется в его конструкторе таким образом, что ссылка может быть замечена другим потоком), то после построения любой поток который видит, что объекту будет гарантировано видеть ссылки на все конечные поля объекта (в состоянии, которое они были сконструированы) без дальнейшей синхронизации.
В частности, стандарт (http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4) говорит:
Модель использования для окончательных полей проста: установите конечные поля для объекта в этом объектном конструкторе; и не пишите ссылка на объект, который строится в месте, где поток может видеть его до завершения конструктора объекта. Если это, тогда, когда объект рассматривается другим потоком, это нить всегда будет видеть правильно построенную версию этого конечные поля объекта. Он также будет видеть версии любого объекта или массив, на который ссылаются те конечные поля, которые, по крайней мере, являются последними как конечные поля.
Они даже приводят следующий пример:
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x; // guaranteed to see 3
int j = f.y; // could see 0
}
}
}
В котором поток A должен запускать "reader()" , и поток B должен запускать "writer()".
До сих пор так хорошо, по-видимому.
Моя главная проблема связана с... действительно ли это на практике? Насколько я знаю, для создания потока A (который работает "reader()" ) см. Ссылку на "f" , мы должны использовать какой-то механизм синхронизации, например, сделать f volatile или использовать блокировки для синхронизации доступа к е. Если мы этого не сделаем, мы даже не гарантируем, что "reader()" сможет увидеть инициализированное "f" , то есть, поскольку у нас не синхронизирован доступ к "f" , читатель потенциально увидит "null" вместо объекта, который был создан потоком записи. Этот вопрос указан в http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalWrong, который является одной из основных ссылок для модели памяти Java [смелый акцент мой]:
Теперь, сказав все это, если после того, как поток построит неизменный объект (то есть объект, который содержит только конечные поля), вы хотите убедиться, что все это правильно видно нить, вам все равно обычно нужно использовать синхронизацию. Нет другим способом обеспечить, например, то, что ссылка на неизменяемую объект будет видеть второй поток. Гарантии программы получает от окончательных полей, следует тщательно закалять с помощью глубоких и тщательное понимание того, как concurrency управляется в вашем коде.
Итак, если нам даже не гарантировано увидеть ссылку на "f" , и поэтому мы должны использовать типичные механизмы синхронизации (volatile, locks и т.д.), и эти механизмы уже приводят к тому, что голы данных уходят, необходимость для финала я бы даже не подумал. Я имею в виду, если для того, чтобы сделать "f" видимым для других потоков, нам все равно нужно использовать энергозависимые или синхронизированные блоки, и они уже делают внутренние поля видимыми для других потоков... что точка (в условиях безопасности потоков) в в первую очередь, финал поля?