Будет ли объект безопасно опубликован?
1) volatile DataObject obj = new DataObject();
2) synchronized(this) {
obj = new DataObject();
}
3) synchronized(this) {
DataObject temp = new DataObject();
temp.initField1()
temp.initField2()
obj = temp
}
DataObject не является неизменяемым (имеет не финальные поля), публикуется в потоке 1 и читается в потоке 2. Чтение DataObject из второго потока не синхронизировано.
Варианты публикации 1) и 2) не являются безопасными, а будет ли безопасен вариант 3? (тут нужна гарантия видимости ссылки и состояния объекта, а также гарантии что объект не будет опубликован раньше завершения инициализации в результате реордеринга и т.п.)
Изменил вопрос чтобы не было разночтений:
final class ImmutableDataObject {
private final int field;
public ImmutableDataObject(int data) {
field = data;
}
public int getField() {
return field;
}
}
class DataObject {
private int field1;
public DataObject(int data) {
field1 = data;
}
public int getField1() {
return field1;
}
}
class DataObject2 extends DataObject {
private int field2;
public DataObject2(int data) {
super(data);
}
void initField2(int data) {
field2 = data;
}
public int getField2() {
return field2;
}
}
class Test1 {
public volatile ImmutableDataObject obj = new ImmutableDataObject(1);
}
class Test2 {
public volatile DataObject obj = new DataObject(1);
}
class Test3 {
public volatile DataObject obj;
public synchronized void init() {
obj = new DataObject(1);
}
}
class Test4 {
public volatile DataObject2 obj;
public void init() {
DataObject2 temp = new DataObject2(1);
temp.initField2(2);
obj = temp;
}
}
Test1: публикация безопасна т.к. объект неизменяемый
Test2 и Test3: публикация не безопасна т.к. внутреннее состояние объекта видимо через "гонку" и может быть любым
Test4: Комментаторы ниже сошлись на мнении, что публикация в Test4 безопасна и синхронизация не нужна т.к. объект создается локально и другие потоки не могут увидеть его в недостроенном состоянии. Запись ссылки в volatile переменную атомарна и гарантирует отсутствие переупорядочиваний со стороны компилятора и процессора т.о. другие потоки всегда будут видеть корректное состояние объекта.
Есть другое мнение или найдена ошибка? Комментарии приветствуются!!!
Ответы (2 шт):
Варианты (1) и (2) неполны, за них трудно что-то сказать, а вариант (3) несколько излишен, согласно модели памяти Java, первоначально введённой JSR-133 п. 3 достаточно:
DataObject temp = new DataObject();
temp.initField1(); // (а)
temp.initField2(); // (b)
volatile DataObject obj = temp;
После чего, в любом потоке выражения вида:
obj.field1
Будут состоять в отношении "происходит - до" (happens-before) с операторами (a) и (b).
Грубо говоря, запись в volatile переменную Java имеет семантику освобождения блокировки, а её чтение - семантику захвата блокировки.
P.S.
О новой версии вопроса.
Test1 - volatile в тесте излишен, скажем, можете поставить final для "порядка".
Test2 - корректен, т.к. field1 = data; происходит-до
volatile DataObject obj = ... , которое происходит-до выражений obj.getField1() в любых потоках.
Test3 - synchronized излишен, тест условно корректен, в том смысле, что не исключён доступ к obj до присвоения. (Как вариант, излишен volatile при условии доступа к obj под synchronized, тогда цепочка отношений происходит-до строится по выходам/входам из/в synchronized).
Test4 - условно корректен, в том смысле, что не исключён доступ к obj до присвоения.
P.P.S.
ИМХО, зря не рассмотрели вариант public final DataObject obj = new DataObject(1);.
Да, объект в третьем коде будет безопасно опубликован. synchronized не нужен.
Общий код:
volatile DataObject obj;
Первая нитка:
DataObject temp = new DataObject();
temp.initField1();
temp.initField2();
obj = temp; // всё что выше будет вычислено до этого присвоения
Вторая нитка:
if (obj != null) {
... = obj.field1;
}
obj объявлен как volatile что приводит к
- компилятор не может переставить
temp.initField2();после присвоенияobj. Всё что написано до присвоения должны быть выполнено полностью до присвоения. - в момент приcвоения все кэши первой нитки сбрасываются в основную память.
- в момент чтения во второй нитке, все кеши второй нитки обновляются из памяти.
То есть имеем:
- объект и все его поля полностью вычислены;
- объект и все его поля полностью записаны в основную память;
- вторая нить видит всё состояние объекта, потому что кэши второй нитки полностью синхронизированы с основной памятью.
Так что всё будет работать правильно. Но есть вещь которую надо хорошенько запомнить: каждое чтение и запись volatile переменной сбрасывает кэши процессора, то есть тормозит не по-детски если у вас интенсивные вычисления.