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

Явная блокировка Java

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

Я просмотрел много сообщений и статей по этому вопросу и понял потенциальные проблемы с получением ссылки на частично сконструированный объект, и, насколько я могу судить, я не думаю, что моя реализация подвержена этим проблемам, Есть ли проблемы со следующим шаблоном?

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

public class Test {
    private static Test instance;
    private static boolean initialized = false;

    public static Test getInstance() {
        if (!initialized) {
            synchronized (Test.class) {
                if (!initialized) {
                    instance = new Test();
                    initialized = true;
                }
            }
        }
        return instance;
    }
}
4b9b3361

Ответ 1

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

EDIT: Чтобы прояснить приведенный выше ответ, исходный вопрос задал вопрос об использовании логического элемента для управления блокировкой двойной проверки. Без решений в приведенной выше ссылке это не сработает. Вы можете дважды заблокировать блокировку, фактически устанавливая логическое значение, но у вас все еще есть проблемы с переупорядочением команд, когда дело доходит до создания экземпляра класса. Предлагаемое решение не работает, потому что экземпляр не может быть инициализирован после того, как вы увидите инициализированное логическое значение как true в несинхронизированном блоке.

Правильное решение для двойной проверки блокировки - либо использовать volatile (в поле экземпляра), либо забыть об инициализированном логическом значении, и обязательно использовать JDK 1.5 или выше или инициализировать его в конечном поле, как это было разработано в связанной статье и в ответ Том, или просто не используйте его.

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

Ответ 2

Это будет работать, если initialized был volatile. Как и в случае synchronized, интересные эффекты volatile на самом деле не так много связаны с ссылкой, как то, что мы можем сказать о других данных. Настройка поля instance и объекта Test принудительно произойдет - до записи в initialized. При использовании кэшированного значения через короткое замыкание происходит чтение initialize - перед чтением instance и объектов, достигнутых через ссылку. Нет существенной разницы в наличии отдельного флага initialized (кроме того, что он вызывает еще большую сложность в коде).

(Правила для полей final в конструкторах для небезопасной публикации немного отличаются.)

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

Код слишком сложный. Вы можете просто написать его как:

private static final Test instance = new Test();

public static Test getInstance() {
    return instance;
}

Ответ 3

Двойная проверка блокировки действительно нарушена, и решение проблемы на самом деле проще реализовать по коду, чем эта идиома - просто используйте статический инициализатор.

public class Test {
    private static final Test instance = createInstance();

    private static Test createInstance() {
        // construction logic goes here...
        return new Test();
    }

    public static Test getInstance() {
        return instance;
    }
}

Статический инициализатор гарантированно будет выполняться в первый раз, когда JVM загружает класс и до того, как ссылка на класс может быть возвращена в любой поток, что делает его по сути потоковым.

Ответ 4

Вот почему нарушена двойная блокировка блокировки.

Синхронизировать гарантии, что только один поток может вводить блок кода. Но это не гарантирует, что изменения переменных, выполненные в синхронизированном разделе, будут видны другим потокам. Только потоки, входящие в синхронизированный блок, гарантированно будут видеть изменения. Именно по этой причине блокировка с двойным контролем блокирована - она ​​не синхронизируется со стороны читателя. Поток чтения может видеть, что singleton не является нулевым, но однотонные данные могут быть не полностью инициализированы (видимы).

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

Конечные статические поля класса не обязательно должны быть неустойчивыми. В Java этот JVM выполняет эту задачу.

Посмотрите мой пост, ответ на шаблон Singleton и сломанную двойную проверку блокировки в реальном Java-приложении, иллюстрирующий пример одноэлементности в отношении двух- проверенный замок, который выглядит умным, но сломан.

Ответ 5

Вероятно, вы должны использовать атомные типы данных в java.util.concurrent.atomic.

Ответ 6

Если "initialized" is true, то "instance" ДОЛЖЕН быть полностью инициализирован, так же как 1 плюс 1 равно 2:). Поэтому код правильный. Экземпляр создается только один раз, но функцию можно назвать миллион раз, поэтому она улучшает производительность без проверки синхронизации на миллион минус один раз.

Ответ 7

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

  1. Во-первых, если вам действительно не нужен синглтон, и двойная проверка используется просто для НЕ создания и инициализации многих объектов.
  2. final поле установлено в конце конструктора/инициализированного блока (что приводит к тому, что все ранее инициализированные поля будут видны другим потокам).

Ответ 8

Я изучаю двойную проверенную блокировку идиомы и из того, что я понял, ваш код может привести к проблеме чтения частично сконструированного экземпляра. ЕСЛИ ваш тестовый класс неизменен:

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

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

(Цитаты из самой рекомендуемой книги Java Concurrency на практике)

Итак, в этом случае будет работать двойная проверенная блокировка idiom.

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

Логическая переменная не добавляет ничего, чтобы избежать проблемы, потому что она может быть установлена ​​в true до того, как будет инициализирован класс Test (ключевое слово synchronized не позволяет полностью переупорядочить, некоторые смены могут изменить порядок). Существует не так - прежде, чем правило в модели памяти Java, чтобы гарантировать это.

И создание логической volatile ничего не добавит, потому что 32-битные переменные создаются атомарно в Java. Двойная проверенная блокировка идиомы также будет работать с ними.

Начиная с Java 5, вы можете исправить эту проблему, объявив переменную экземпляра как изменчивую.

Вы можете больше узнать о двойной проверенной идиоме в этой очень интересной статье.

Наконец, некоторые рекомендации, которые я прочитал:

  • Рассмотрим, следует ли использовать шаблон singleton. Многие считают, что это анти-шаблон. Предпочтительная инъекция предпочтительна там, где это возможно. Проверьте этот.

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

Если вам все еще нужно выполнить эту оптимизацию, проверьте эту ссылку которая предоставляет некоторые альтернативы для достижения аналогичного эффекта с тем, что вы пытаетесь сделать.

Ответ 9

Проблема DCL нарушена, хотя она работает на многих виртуальных машинах. Существует хорошая запись о проблеме здесь http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html.

многопоточность и когерентность памяти - более сложные вопросы, чем они могут появиться. [...] Вы можете игнорировать всю эту сложность, если просто используете инструмент, который Java предоставляет именно для этой цели - синхронизация. Если вы синхронизируете каждый доступ к переменной, которая могла быть написана или может быть прочитана другим потоком, у вас не будет проблем с когерентностью памяти.

Единственный способ правильно решить эту проблему - избежать ленивой инициализации (делать это с нетерпением) или провести единую проверку внутри синхронизированного блока. Использование логического initialized эквивалентно нулевой проверке самой ссылки. Второй поток может видеть, что initialized является истинным, но instance может все еще быть нулевым или частично инициализированным.

Ответ 10

Двойная проверка блокировки - это анти-шаблон.

Lazy Classing Holder Class - это шаблон, на который вы должны смотреть.

Несмотря на множество других ответов, я решил, что должен ответить, потому что все еще не один простой ответ, который говорит, почему DCL нарушается во многих контекстах, почему это не нужно и что вы должны делать вместо этого. Поэтому я буду использовать цитату из Goetz: Java Concurrency In Practice, которая для меня дает самое сукчистское объяснение в его последней главе о модели памяти Java.

Это о безопасной публикации переменных:

Реальная проблема с DCL - это предположение, что худшее, что может случиться при чтении ссылки на общий объект без синхронизации, - это ошибочно увидеть устаревшее значение (в данном случае - null); в этом случае идиома DCL компенсирует этот риск, снова попробовав блокировку. Но худший случай на самом деле значительно хуже - можно увидеть текущее значение ссылочных, но устаревших значений для состояния объекта, что означает, что объект может считаться недействительным или неправильным.

Последующие изменения в JMM (Java 5.0 и новее) позволили DCL работать, если ресурс стал волатильным, а влияние на производительность этого мало, поскольку волатильные чтения обычно немного дороже, чем энергонезависимые.

Однако это идиома, чья полезность в значительной степени прошла - силы, которые ее мотивировали (медленная незащищенная синхронизация, медленный запуск JVM), больше не играют в игру, что делает ее менее эффективной как оптимизация. Ленькая идиома держателя инициализации предлагает те же преимущества и легче понять.

Листинг 16.6. Ленивый Инициализационный Класс Идиомы.

public class ResourceFactory
    private static class ResourceHolder {
        public static Resource resource = new Resource();
    }

    public static Resource getResource() {
        return ResourceHolder.resource;
    }
}

Это способ сделать это.

Ответ 11

Во-первых, для одиночных чисел вы можете использовать Enum, как объяснено в этом вопросе Реализация Singleton с Enum (на Java)

Во-вторых, начиная с Java 1.5 вы можете использовать изменчивую переменную с двойной проверенной блокировкой, как описано в конце этой статьи: https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html