Подтвердить что ты не робот

Ленивая инициализация без синхронизации или ключевое слово volatile

На днях Howard Lewis Ship опубликовал запись в блоге под названием "Что я узнал в Hacker Bed and Breakfast" , одним из пунктов является

Поле экземпляра Java, которое назначается ровно один раз через ленивый инициализация не должна быть синхронизирована или нестабильной (так долго так как вы можете принять условия гонки по потокам, чтобы назначить поле); это от богатого хикки

На первый взгляд это противоречит принятой мудрости о видимости изменений в памяти по потокам, и если это описано в книге Java Concurrency в Практике или в спецификации языка Java, я пропустил ее, Но это было что-то, что ХСЛ получила от Рика Хики на мероприятии, где присутствовал Брайан Гетц, так что, похоже, что-то должно быть что-то. Может ли кто-нибудь объяснить логику этого заявления?

4b9b3361

Ответ 1

Это утверждение звучит немного загадочно. Тем не менее, я думаю, HLS относится к случаю, когда вы лениво инициализируете поле экземпляра и не заботитесь, если несколько потоков выполняют эту инициализацию более одного раза.
В качестве примера я могу указать метод hashCode() класса String:

private int hashCode;

public int hashCode() {
    int hash = hashCode;
    if (hash == 0) {
        if (count == 0) {
            return 0;
        }
        final int end = count + offset;
        final char[] chars = value;
        for (int i = offset; i < end; ++i) {
            hash = 31*hash + chars[i];
        }
        hashCode = hash;
    }
    return hash;
}

Как вы видите, доступ к полю hashCode (который хранит кешированное значение вычисленного хеша строки) не синхронизирован, и поле не объявлено как volatile. Любой поток, который вызывает метод hashCode(), все равно получит одно и то же значение, хотя поле hashCode может быть написано несколько раз разными потоками.

Этот метод имеет ограниченное удобство. IMHO используется в основном для таких случаев, как в примере: кешированный примитивный/неизменный объект, который вычисляется из остальных конечных/неизменяемых полей, но его вычисление в конструкторе является избыточным.

Ответ 2

Edit:

Хмм. Как я читал, это технически некорректно, но на практике с некоторыми оговорками. Только окончательные поля могут быть безопасно инициализированы один раз и доступны в нескольких потоках без синхронизации.

Lazy инициализированные потоки могут страдать от проблем синхронизации несколькими способами. Например, вы можете иметь условия гонки конструктора, в которых ссылка класса была экспортирована без полного инициализации класса.

Я думаю, что это сильно зависит от того, есть ли у вас примитивное поле или объект. Примитивные поля, которые могут быть инициализированы несколько раз, когда вы не возражаете, что несколько потоков инициализации будут работать нормально. Однако инициализация стиля HashMap таким образом может быть проблематичной. Даже значения long на некоторых архитектурах могут хранить разные слова в нескольких операциях, поэтому могут экспортировать половину значения, хотя я подозреваю, что long никогда не пересечет страницу памяти, поэтому это никогда не произойдет.

Я думаю, что это сильно зависит от того, имеет ли приложение какие-либо барьеры памяти - любые блоки synchronized или доступ к полям volatile. Дьявол, безусловно, здесь, и код, который выполняет ленивую инициализацию, может отлично работать в одной архитектуре с одним набором кода, а не в другой модели потока или с приложением, которое редко синхронизируется.


Вот хороший пример для конечных полей:

http://www.javamex.com/tutorials/synchronization_final.shtml

Начиная с Java 5, одно конкретное использование ключевого слова final является очень важным и часто пропускаемым оружием в вашем арсенале concurrency. По сути, final можно использовать, чтобы убедиться, что при создании объекта другой поток, обращающийся к этому объекту, не видит этот объект в частично сконструированном состоянии, как это могло бы произойти иначе. Это связано с тем, что при использовании в качестве атрибута переменных объекта final имеет следующую важную характеристику как часть своего определения:

Теперь, даже если поле отмечено окончательным, если это класс, вы можете изменить поля внутри класса. Это другая проблема, и для этого у вас должна быть синхронизация.

Ответ 3

Это работает отлично при некоторых условиях.

  • его можно попробовать и установить поле более одного раза.
  • это нормально, если отдельные потоки видят разные значения.

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

private static Properties prop = null;

public static Properties getProperties() {
    if (prop == null) {
        prop = new Properties();
        try {
            prop.load(new FileReader("my.properties"));
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }
    return prop;
}

В краткосрочной перспективе это менее эффективно, чем использование блокировки, но в долгосрочной перспективе оно может быть более эффективным. (Хотя свойства имеют собственный замок, но вы получаете идею;)

IMHO, это не решение, которое работает во всех случаях.

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

Ответ 4

Я думаю, что утверждение неверно. Другой поток может видеть частично инициализированный объект, поэтому ссылка может быть видима для другого потока, даже если конструктор не закончил работу. Это описано в Java Concurrency на практике, раздел 3.5.1:

public class Holder {

    private int n;

    public Holder (int n ) { this.n = n; }

    public void assertSanity() {
        if (n != n)
            throw new AssertionError("This statement is false.");
    }

}

Этот класс не является потокобезопасным.

Если видимый объект неизменен, то я уверен, из-за семантики конечных полей вы не увидите их до завершения его конструктора (раздел 3.5.2).