• Java, многопоточность, инициализация объектов и reordering — всегда ли нужно синхронизировать инициализацию?

    @NikKotovski Автор вопроса
    Почитав ещё какое-то время о вопросе и подробнее разобрав некоторые статьи, я разобрал вопрос, и могу дать на него ответ самостоятельно. Проще всего разобрать его на статье из Википедии:
    https://en.wikipedia.org/wiki/Double-checked_locki...

    Статья говорит, что

    Due to the semantics of some programming languages, the code generated by the compiler is allowed to update the shared variable to point to a partially constructed object before A has finished performing the initialization. For example, in Java if a call to a constructor has been inlined then the shared variable may immediately be updated once the storage has been allocated but before the inlined constructor initializes the object.

    То есть JVM и правда может отдать ссылку на объект вовне до того, как пройдёт инициализация его полей и будет выполнен код в блоках инициализации и конструкторе. Более того

    One of the dangers of using double-checked locking in J2SE 1.4 (and earlier versions) is that it will often appear to work: it is not easy to distinguish between a correct implementation of the technique and one that has subtle problems. Depending on the compiler, the interleaving of threads by the scheduler and the nature of other concurrent system activity, failures resulting from an incorrect implementation of double-checked locking may only occur intermittently. Reproducing the failures can be difficult.

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

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

    Что касается вопроса про метод, создающий объект, то нет, данный метод не может вернуть недоинициализированный объект. Т.е. в переменную может попасть ссылка на недоинициализированный объект, но пока не пройдёт инициализация код дальше в данном потоке выполняться не будет. Т.о. return вернёт уже ссылку на полностью готовый объект, потому что m локальная переменная и хранится строго внутри самого потока, и никто к ней не получит доступ, пока она не инициализируется полностью.

    Ну и ещё одна вещь, которая следует из всего сказанного выше: если вам нужно гарантированное создание объекта, который точно будет доинициализирован, когда к нему обратится другой поток, но вы не хотите использовать синхронизаций или volatile и готовы пожертвовать частью сделанных в нём изменений, то можно просто сделать так:
    if (m == null) {
    MyClass n = new MyClass();
    m = n;
    }
    В этом случае сначала класс инициализируется в локальной переменной, а потом уже спокойно будет передан общей переменной. Но, ещё раз, от создания нескольких объектов и потери изменений этот метод не убережёт.
    Ответ написан
    Комментировать