Частично построенный объект / Многопоточность

Я использую joda из-за хорошей репутации в отношении многопоточности. Для того чтобы сделать многопоточную обработку дат эффективной, необходимо пройти долгий путь, например, сделав неизменными все объекты Date/Time/DateTime.

Но вот ситуация, когда я не уверен, действительно ли Йода поступает правильно. Возможно, так и есть, но мне очень интересно увидеть объяснение.

Когда toString() DateTime вызывается, Joda делает следующее:

/* org.joda.time.base.AbstractInstant */
public String toString() {
    return ISODateTimeFormat.dateTime().print(this);
}

Все средства форматирования являются потокобезопасными (они также неизменяемы), но как насчет фабрики форматирования:

private static DateTimeFormatter dt;

/*  org.joda.time.format.ISODateTimeFormat */
public static DateTimeFormatter dateTime() {
    if (dt == null) {
        dt = new DateTimeFormatterBuilder()
            .append(date())
            .append(tTime())
            .toFormatter();
    }
    return dt;
}

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

Я вижу следующие опасности:

  • Состояние гонки при нулевой проверке -> наихудший случай: создаются два объекта.

Нет проблем, так как это исключительно вспомогательный объект (в отличие от обычной ситуации с одноэлементным шаблоном), один сохраняется в dt, другой теряется и рано или поздно будет собирать мусор.

  • статическая переменная может указывать на частично построенный объект до завершения инициализации объекта

(прежде чем называть меня сумасшедшим, прочитайте о похожей ситуации в этой статье в Википедии.)

Так как же Joda гарантирует, что частично созданный форматер не будет опубликован в этой статической переменной?

Спасибо за ваши объяснения!

Рето

4 ответа

Решение

Вы сказали, что форматеры только для чтения. Если они используют только конечные поля (я не читал источник форматирования), то в 3-й редакции спецификации языка Java они защищены от создания частичных объектов с помощью "Окончательной семантики поля". Я не проверял 2-е издание JSL и не уверен, правильна ли такая инициализация в этом издании.

Посмотрите на главы 17.5 и 17.5.1 в JLS. Я создам "цепочку событий" для требуемого отношения "происходит до".

Прежде всего, где-то в конструкторе есть запись в последнее поле в форматере. Это запись w. Когда конструктор завершает свою работу, действие "заморозить" происходит. Давайте назовем это ф. Где-то позже в порядке программы (после возврата из конструктора, может быть, некоторых других методов и возврата из toFormatter) происходит запись в поле dt. Давайте дадим этому написать имя. Эта запись (a) идет после действия замораживания (f) в "программном порядке" (порядок в однопоточном выполнении) и, таким образом, f происходит перед a (hb(f, a)) просто по определению JLS. Фу, инициализация завершена...:)

Несколько позже, в другом потоке, происходит вызов формата dateTime(). В то время нам нужно два чтения. Первым из двух является чтение окончательной переменной в объекте форматирования. Давайте назовем это r2 (чтобы соответствовать JLS). Второе из двух - это чтение "этого" для Formatter. Это происходит во время вызова метода dateTime () при чтении поля dt. И давайте назовем это читать r 1. Что у нас сейчас? Читайте r 1 видел некоторые записи к DT. Я считаю, что эта запись была действием a из предыдущего абзаца (только один поток написал это поле, просто для простоты). Так как r 1 видит запись a, то существует mc(a, r1) (отношение "цепочка памяти", определение первого предложения). Текущий поток не инициализировал средство форматирования, считывает его поле в действии r2 и видит "адрес" считывателя форматера в действии r 1. Таким образом, по определению, существует разыменование (r1, r2) (другое действие, упорядоченное из JLS).

Мы должны написать перед заморозкой, hb(w, f). У нас есть заморозка перед назначением dt, hb(f, a). У нас есть чтение из dt, mc(a, r1). И у нас есть цепочка разыменования между r 1 и r2, разыменования (r1, r2). Все это приводит к соотношению hb(w, r2), которое происходит раньше, просто по определению JLS. Также по определению hb(d, w) где d - запись значения по умолчанию для конечного поля в объекте. Таким образом, чтение r2 не может видеть запись w и должно видеть запись r2 (единственная запись в поле из программного кода).

То же самое относится и к более косвенному доступу к полю (конечное поле объекта хранится в последнем поле и т. Д.).

Но это еще не все! Нет доступа к частично построенному объекту. Но есть более интересная ошибка. В отсутствие какой-либо явной синхронизации dateTime () может возвращать ноль. Я не думаю, что такое поведение можно наблюдать на практике, но 3-е издание JLS не предотвращает такое поведение. Первое чтение поля dt в методе может увидеть значение, инициализированное другим потоком, но второе чтение dt может увидеть "запись значения по умолчанию". Не бывает - до того, как существуют отношения, чтобы предотвратить это. Такое возможное поведение характерно для 3-го издания, во втором издании есть "запись в основную память"/"чтение из основной памяти", которое не позволяет потоку видеть значения переменной, возвращающиеся во времени.

Это немного не ответ, но самое простое объяснение

Так как же Joda гарантирует, что частично созданный форматер будет опубликован в этой статической переменной?

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

Я задал аналогичный вопрос в списке рассылки Joda в 2007 году, хотя я не нашел ответы на них убедительными, и в результате я избегал времени Joda, к лучшему или к худшему.

Версия 3 спецификации языка Java гарантирует, что обновления ссылок на объекты являются атомарными, независимо от того, являются ли они 32-разрядными или 64-разрядными. Это в сочетании с аргументами, изложенными выше, делает код Joda поточно-ориентированным IMO (см. Java.sun.com/docs/books/jls/third_edition/html/memory.html#17.7).

IIRC, версия 2 JLS, не включала в себя такое же явное разъяснение об объектных ссылках, то есть только 32-битные ссылки были гарантированно атомарными, поэтому, если вы использовали 64-битную JVM, не было никакой гарантии, что это сработает. В то время я использовал Java 1.4, которая предшествовала JLS v3.

ИМО в худшем случае это не два объекта, которые создаются, а несколько (столько, сколько есть потоки, вызывающие dateTime(), точнее). поскольку dateTime() не синхронизируется и dt не является ни окончательным, ни изменчивым, изменение его значения в одном потоке не гарантируется для других потоков. Так что даже после того, как один поток инициализирован dtлюбое количество других потоков может по-прежнему видеть ссылку как ноль, тем самым успешно создавая новые объекты.

Кроме того, как объяснено другими, частично созданный объект не может быть опубликован dateTime(), Частично измененная ( = висящая) ссылка также не может быть, так как обновления ссылочных значений гарантированно будут атомарными.

Другие вопросы по тегам