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


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

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

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

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

Все форматеры являются thread safe (они также неизменяемы), но что насчет formatter-factory:

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;
}

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

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

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

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

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

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

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

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

Reto

4   5   2010-03-24 20:15:31

4 ответа:

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

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

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

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

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

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

Но это еще не все! Нет доступа к частично построенному объекту. Но есть и более интересная ошибка. При отсутствии явной синхронизации функция dateTime () может возвращать значение null. Я не думаю, что такое поведение можно наблюдать на практике, но JLS 3-й выпуск не препятствует такому поведению. При первом чтении поля 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, любое количество других потоков может по-прежнему видеть ссылку как null, таким образом, счастливо создавать новые объекты.

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