Как JVM подразумеваемые барьеры памяти ведут себя при создании цепочки?

Ссылаясь на мой более ранний вопрос о не полностью сконструированных объектах, у меня есть второй вопрос. Как указал Джон Скит, в конце конструктора есть неявный барьер памяти, который гарантирует, что поля final видны для всех потоков. Но что, если конструктор вызывает другой конструктор; есть ли такой барьер памяти в конце каждого из них или только в конце того, что вызвано в первую очередь? То есть, когда "неправильное" решение:

public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e);
                }
            });
    }
}

И правильным будет версия метода factory:

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() гарантированно фактически называет закрытый конструктор выше (в этом случае был бы барьер, где предполагалось, и все было бы безопасно), или это возможно, что частный конструктор встраивается в публичный как оптимизация для сохранения одного барьера памяти (в этом случае не было бы барьера до тех пор, пока в конце публичного конструктора)?

Определены ли правила this() точно где-то? Если нет, то я думаю, что мы должны предположить, что допускаются встроенные цепные конструкторы, и, вероятно, некоторые JVM или, возможно, даже javac делают это.

Ответы

Ответ 1

Я думаю, что это безопасно, поскольку модель памяти Java утверждает, что:

Пусть o - объект, а c - конструктор для o, в котором конечный поле f записано. Выполняется действие замораживания на конечном поле f of o когда c выходит, как обычно, так и внезапно. Заметим, что если один конструктор вызывает другой конструктор, а вызываемый конструктор устанавливает конечное поле, замораживание для конечного поля происходит на конец вызываемого конструктора.

Ответ 2

Объект считается полностью инициализированным, когда его конструктор заканчивается.

Это относится также к цепным конструкторам.

Если вам нужно зарегистрироваться в конструкторе, определите слушателя как статический внутренний класс. Это безопасно.

Ответ 3

Ваша вторая версия неверна, поскольку она позволяет ссылаться на 'this' на отказ от процесса построения. Сбой 'this' приводит к недействительности гарантий безопасности инициализации, которые обеспечивают безопасность конечных полей.

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

Ответ 4

РЕДАКТИРОВАТЬ. После комментария, который предложил компилятору встраивать частный конструктор (я не думал об этой оптимизации), вероятность того, что код будет небезопасным. И худшая часть небезопасного многопоточного кода - это, похоже, работает, поэтому вам лучше избегать его полностью. Если вы хотите сыграть разные трюки (вы действительно хотите избежать factory по какой-либо причине), подумайте о добавлении обертки, чтобы гарантировать согласованность данных во внутреннем объекте реализации и зарегистрировать внешний объект.


Я предполагаю, что он будет хрупким, но нормально. Компилятор не может знать, будет ли внутренний конструктор вызываться только изнутри других конструкторов или нет, поэтому он должен убедиться, что результат будет правильным для кода, вызывающего только внутренний конструктор, поэтому какой бы механизм он не использовал (барьер памяти?), там будет.

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

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

Кстати: предполагаемая безопасность может быть неправильной. Я не знаю, насколько сложный/умный компилятор, и является ли барьер памяти (или тому подобное) тем, что он мог бы попытаться оптимизировать... поскольку конструктор является частным, у компилятора есть достаточно информации, чтобы знать, что это только вызываемый из других конструкторов, и это достаточно информации, чтобы определить, что механизм синхронизации не нужен во внутреннем конструкторе...

Ответ 5

Исходная ссылка объекта в c-tor может публиковать неполностью сконструированный объект. Это верно, даже если публикация является последним утверждением в конструкторе.

Ваш SafeListener может не работать нормально в параллельной среде, даже если выполняется inlining c-tor (что, по моему мнению, нет), подумайте о создании объектов, используя отражение, обратившись к частному c-tor).