@NikKotovski

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

Java создаёт ссылку на инициализируемый объект до конструктора и до блоков инициализации. Данное обстоятельство позволяет изнутри этих блоков отдать ссылку на объект другому объекту и другому потоку с помощью слова this до того, как все полня будут инициализированы, если писать код неаккуратно - если сначала отдать ссылку, а потом инициализировать поля. А ещё логика работы Java позволяет виртуальной машине менять инструкции местами, поэтому даже если писать код аккуратно и передавать ссылку в коде после того, как там будет прописана вся инициализация, то это всё равно не защищает от передачи ссылки до инициализации полей, потому что JVM может выполнить код, осуществляющий передачу ссылки до того, как выполнится инициализация. В принципе с этим всё понятно.

Для меня вопрос состоит в том, может ли конструктор вернуть ссылку на себя вовне до того, как он инициализировался, если мы не используем слово this - то есть при обычной инициализации. И если да, то нельзя ли избежать этого с помощью создания метода, где объект будет инициализироваться, а потом возвращаться. Я перекопал весь интернет в поисках ответа, и как правило пишут о том, что такое возможно, и что оба варианта инициализации не являются потоково-безопасными. Проблема в том, что большая часть из ответов старые и логика синхронный работы с тех пор менялась раз или два. Более того, среди более свежих заметок по этой теме мне уже встречались обе точки зрения - и я не могу понять, то ли те несколько заметок, которые говорят, что это безопасно, ошибочны, то ли ошибается большинство, которое по инерции считает, что инициализация потоково-небезопасна.

Приведу конкретный пример того, о чём идёт речь. Пусть у нас есть переменная
MyClass m;
доступная для нескольких потоков. Если эта переменная не инициализирована, то один из потоков пытается её инициализировать
m = new MyClass();
Может ли переменная m начать указывать на какой-то объект, до того, как он будет инициализирован. Другими словами, может ли конструктор вернуть m ссылку на содержащий его объект, до того, как в нём выполнится весь код.

Допустим может. Тогда второй пример. Пусть у нас есть метод
MyClass creatMyClass() {
MyClass m = new MyClass();
return m;
}
Может ли данный метод вернуть m, которая будет не до конца инициализирована?

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

Т.о. вот конкретный вопрос - может ли в первых двух примерах (кода) переменная m содержать ссылку на объект с недоинициализированными полями, если m не volatile? И если да, то поможет ли volatile избежать проблемы?
  • Вопрос задан
  • 441 просмотр
Решения вопроса 1
@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;
}
В этом случае сначала класс инициализируется в локальной переменной, а потом уже спокойно будет передан общей переменной. Но, ещё раз, от создания нескольких объектов и потери изменений этот метод не убережёт.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
@kuftachev
Подумайте сами, как может конструктор отработать на половину? Он же вернет ссылку на результат своей работы вызываемому коду когда исполнится весь. Конечно с дуру можно и член сломать, но зачем Вам в конструкторе передавать указатель на себя еще куда-то?
По Вашему примеру рождается вопрос, не идет ли речь про Singleton? Если да, то 2 из 4 способов реализации этого паттерна описанные в книге Джошуа Блоха потокобезопасные.

P.S. Суть volatile в том, что Вы говорите не кешировать это значение в процессоре, а всегда спускаться в память (кстати, тут я не знаю, если в системе один процессор Intel, у них в отличии от AMD кеш третьего уровня общий, будет ли использован он или все равно только память), чтобы работать со свежим значением. Без этого два ядра могут долго работать со своими значениями, но вот атомарность записи этого значения не гарантируется. То есть, например int будет атомарно меняться, а вот long уже нет. Поэтому, на счет ссылок не уверен, они вроде бы тоже 64 бита, по идее, они тоже должны быть не атомарны, но может там другая защита, а то создадим два объекта и получим ссылку непонятно куда.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы