Перейти к основному содержимому

Двойная проверка блокировки с помощью Singleton

· 3 мин. чтения

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

В этом случае класс InstanceHolder назначит поле при первом доступе к нему, вызвав getInstance.

3.3. Перечисление Синглтон

Последнее решение взято из книги Джошуа Блока Effective Java (статья 3) и использует перечисление вместо класса . На момент написания статьи это считалось наиболее лаконичным и безопасным способом написания синглтона:

public enum EnumSingleton {
INSTANCE;

// other methods...
}

4. Вывод

Подводя итог, в этой краткой статье мы рассмотрели тщательно проверенный шаблон блокировки, его ограничения и некоторые альтернативы.

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

Как всегда, код всех примеров доступен на GitHub .