Будет ли объект безопасно опубликован?

Рейтинг: 1Ответов: 2Опубликовано: 22.07.2025
    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 переменную атомарна и гарантирует отсутствие переупорядочиваний со стороны компилятора и процессора т.о. другие потоки всегда будут видеть корректное состояние объекта.

Есть другое мнение или найдена ошибка? Комментарии приветствуются!!!

Ответы

▲ 3

Варианты (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);.

▲ 3

Да, объект в третьем коде будет безопасно опубликован. 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 переменной сбрасывает кэши процессора, то есть тормозит не по-детски если у вас интенсивные вычисления.