Как ведут себя барьеры неявной памяти JVM при цепочке конструкторов?
ссылаясь на мою ранее вопрос о неполно построенных объектах, у меня есть второй вопрос. Как указал Джон Скит, в конце конструктора есть неявный барьер памяти, который гарантирует, что final
поля видны всем потокам. Но что, если конструктор вызывает другой конструктор; есть ли такой барьер памяти в конце каждого из них или только в конце того, который был вызван в первую очередь? То есть, когда "неправильное" решение есть:
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
и правильным будет версия Заводского метода:
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
}
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
будет ли следующее работать тоже или нет?
public class MyListener {
private final EventListener listener;
private MyListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
}
}
public MyListener(EventSource source) {
this();
source.register(listener);
}
}
обновление: основной вопрос заключается в том, что составляет this()
гарантировано на самом деле вызов частный конструктор выше (в этом случае будет барьер, где предполагалось, и все будет безопасно), или возможно, что частный конструктор получает inlined в публичный как оптимизация для сохранения одного барьера памяти (в этом случае не будет барьера до конца публичного конструктора)?
правила this()
определен точно где-то? Если нет, то я думаю, мы должны предположить, что встраивание цепных конструкторов разрешено, и, вероятно, некоторые JVMs или даже javac
s делают это.
5 ответов
Я думаю, что это безопасно, поскольку модель памяти java утверждает, что:
пусть o быть объектом, а c быть конструктором для o в котором финал поле f написано. Действие замораживания на финальном поле f of o происходит когда c выходы, либо нормально, либо резко. Обратите внимание, что если конструктор вызывает другой конструктор, и вызываемый конструктор наборы окончательное поле, замораживание для окончательного поля происходит на конец вызываемого конструктора.
объект считается полностью инициализированным, когда его конструктор завершает работу.
это относится и к цепным конструкторам.
Если вам нужно зарегистрироваться в конструкторе, определите прослушиватель как статический внутренний класс. Это безопасно.
ваша вторая версия неверна, потому что она позволяет "этой" ссылке уйти от процесса строительства. Наличие "этого" побега аннулирует гарантии безопасности инициализации, которые дают конечным полям их безопасность.
чтобы решить неявный вопрос, барьер в конце строительства происходит только в самом конце строительства объекта. Интуиция, которую один читатель предложил о встраивании, является полезной; с точки зрения модели памяти Java, границы метода не существует.
редактировать после комментария, который предложил компилятору встроить частный конструктор (я не думал об этой оптимизации), есть вероятность, что код будет небезопасным. И худшая часть небезопасного многопоточного кода-это то, что, похоже, работает, поэтому вам лучше избегать его полностью. Если вы хотите сыграть разные трюки (вы действительно хотите избежать фабрики по какой-то причине), подумайте о добавлении обертки, чтобы гарантировать согласованность данных во внутреннем реализация объекта и регистрация во внешнем объекте.
Я предполагаю, что он будет хрупким, но в порядке. Компилятор не может знать, будет ли внутренний конструктор вызываться только из других конструкторов или нет, поэтому он должен убедиться, что результат будет правильным для кода, вызывающего только внутренний конструктор, поэтому какой бы механизм он ни использовал (барьер памяти?) должен быть на месте.
Я бы предположил, что компилятор добавит память барьер в конце каждого конструктора. Проблема все еще существует: вы проходите this
ссылка на другой код (возможно, другие потоки), прежде чем он будет полностью построен-это плохо -, но если единственная оставшаяся конструкция регистрирует слушателя, то состояние объекта является стабильным, как это будет когда-либо.
решение хрупкой в какой-то другой день вам или другому программисту может потребоваться добавить другой член к объекту и может забудьте, что цепные конструкторы-это трюк параллелизма и могут решить инициализировать поле в общедоступном конструкторе, и при этом добавят трудную для обнаружения потенциальную гонку данных в вашем приложении, поэтому я постараюсь избежать этой конструкции.
BTW: предполагаемая безопасность может быть неправильной. Я не знаю, насколько сложен / умен компилятор, и является ли барьер памяти (или тому подобное) чем-то, что он может попытаться оптимизировать... поскольку конструктор является частным, компилятор имеет достаточно информации, чтобы знать, что он вызывается только из других конструкторов, и этого достаточно, чтобы определить, что механизм синхронизации не нужен во внутреннем конструкторе...
экранирование ссылки на объект в c-tor может опубликовать неполно построенный объект. Это верно даже Если публикация является последним оператором в конструкторе.
ваш SafeListener может не вести себя нормально в параллельной среде, даже если выполняется вставка c-tor (что, я думаю, не так - подумайте о создании объектов с помощью отражения путем доступа к частному c-tor).