Автор оригинала: Donato Rimenti.
1. введение
В этом уроке мы поговорим о шаблоне проектирования блокировки с двойной проверкой. Этот шаблон уменьшает количество приобретений блокировок, просто предварительно проверяя состояние блокировки. В результате этого обычно повышается производительность.
Давайте глубже рассмотрим, как это работает.
2. Реализация
Для начала давайте рассмотрим простой синглтон с драконовской синхронизацией:
public class DraconianSingleton { private static DraconianSingleton instance; public static synchronized DraconianSingleton getInstance() { if (instance == null) { instance = new DraconianSingleton(); } return instance; } // private constructor and other methods ... }
Несмотря на то, что этот класс потокобезопасен, мы видим, что существует явный недостаток производительности: каждый раз, когда мы хотим получить экземпляр нашего синглтона, нам нужно получить потенциально ненужную блокировку.
Чтобы исправить это, вместо этого мы могли бы начать с проверки того, нужно ли нам создавать объект в первую очередь, и только в этом случае мы получим блокировку.
Идя дальше, мы хотим снова выполнить ту же проверку, как только войдем в синхронизированный блок, чтобы сохранить атомарность операции:
public class DclSingleton { private static volatile DclSingleton instance; public static DclSingleton getInstance() { if (instance == null) { synchronized (DclSingleton .class) { if (instance == null) { instance = new DclSingleton(); } } } return instance; } // private constructor and other methods... }
Одна вещь, которую следует иметь в виду при использовании этого шаблона, заключается в том, что поле должно быть изменчивым для предотвращения проблем с несогласованностью кэша. Фактически, модель памяти Java позволяет публиковать частично инициализированные объекты, и это, в свою очередь, может привести к незначительным ошибкам.
3. Альтернативы
Несмотря на то, что блокировка с двойной проверкой потенциально может ускорить процесс, у нее есть по крайней мере две проблемы:
- поскольку для правильной работы требуется ключевое слово volatile , оно несовместимо с Java 1.4 и более низкими версиями
- это довольно многословно, и это затрудняет чтение кода
По этим причинам давайте рассмотрим некоторые другие варианты без этих недостатков. Все следующие методы делегируют задачу синхронизации JVM.
3.1. Ранняя Инициализация
Самый простой способ обеспечить потокобезопасность-встроить создание объекта или использовать эквивалентный статический блок. Это использует тот факт, что статические поля и блоки инициализируются одно за другим ( Спецификация языка Java 12.4.2 ):
public class EarlyInitSingleton { private static final EarlyInitSingleton INSTANCE = new EarlyInitSingleton(); public static EarlyInitSingleton getInstance() { return INSTANCE; } // private constructor and other methods... }
3.2. Инициализация по требованию
Кроме того, поскольку из ссылки на спецификацию языка Java в предыдущем абзаце мы знаем, что инициализация класса происходит при первом использовании одного из его методов или полей, мы можем использовать вложенный статический класс для реализации ленивой инициализации:
public class InitOnDemandSingleton { private static class InstanceHolder { private static final InitOnDemandSingleton INSTANCE = new InitOnDemandSingleton(); } public static InitOnDemandSingleton getInstance() { return InstanceHolder.INSTANCE; } // private constructor and other methods... }
В этом случае Владелец экземпляра класс назначит поле при первом доступе к нему, вызвав getInstance.
3.3. Перечисление одноэлементных
Последнее решение взято из Эффективной книги Java (Пункт 3) Джошуа Блока и использует перечисление вместо класса . На момент написания этой статьи это считается наиболее кратким и безопасным способом написания синглтона:
public enum EnumSingleton { INSTANCE; // other methods... }
4. Заключение
Подводя итог, в этой краткой статье был рассмотрен дважды проверенный шаблон блокировки, его пределы и некоторые альтернативы.
На практике чрезмерная многословность и отсутствие обратной совместимости делают этот шаблон подверженным ошибкам, и поэтому нам следует избегать его. Вместо этого мы должны использовать альтернативу, которая позволяет JVM выполнять синхронизацию.
Как всегда, код всех примеров доступен на GitHub .